2026年1月の素振り

これは何

2026年1月の単発の記事にするほどでもない雑多な話題をまとめている。

GitHub ActionsでIaCツールをガチャガチャした

飼っているサーバーのお手入れ

手動で管理していたサーバーを構築しなおした。
上述の検証はこのためにあった。
それと以前にミスってローカルのtfstateファイルを飛ばしたことが数回あるため、流石にそろそろCIで運用するか...となった次第。

検証用リポジトリGithub ActionsでIaCツールを動かすことだけを目的としているため中身があまりにもお粗末な状態になっている。
実際に運用しているリポジトリを公開したいところだが、見られたくない情報もギュウギュウに詰まっているため割愛。

サーバーはLinodeを使っている。個人で使うには「お、ねだん以上。」であり、手頃にエンジニアフレンドリー。
Ansibleのユーザーを作るといったゼロイチの構築も手作業で行いたくなかったため、以下のようなStackScriptを書くなどした。

#!/bin/bash
# <UDF name="ansible_public_key" label="ansible user authorized public key" example="ssh-ed25519 AAA..." default="">
apt-get -q update && apt-get -q -y upgrade

ANSIBLE_USER="ansible"
SUDOERS_FILE="/etc/sudoers.d/${ANSIBLE_USER}"

useradd -m -s /bin/bash "${ANSIBLE_USER}"

mkdir -p "/home/${ANSIBLE_USER}/.ssh"
chmod 700 "/home/${ANSIBLE_USER}/.ssh"
echo "${ANSIBLE_PUBLIC_KEY}" | tee "/home/${ANSIBLE_USER}/.ssh/authorized_keys"
chmod 600 "/home/${ANSIBLE_USER}/.ssh/authorized_keys"
chown -R "${ANSIBLE_USER}:${ANSIBLE_USER}" "/home/${ANSIBLE_USER}/.ssh"

echo "${ANSIBLE_USER} ALL=(ALL) NOPASSWD: ALL" | tee "${SUDOERS_FILE}"
chmod 440 "${SUDOERS_FILE}"

これさえやってしまえばソッコーAnsibleで構築を始めることができてハッピー。

Caddyを試した

証明書の取得と更新を自動でやってくれるWebサーバー。
リバースプロキシとして使っている。

先述したサーバーでは、GrafanaやらチョットしたWebツールが走っている。
これまではNginx & Let's Encryptsのよくある構成を組んでいたわけだけど、新しく組むに当たってちゃんと設定するのが面倒に感じていてChatGPTに愚痴っていたら教えてもらった。

これもAnsibleでよしなに設定。ロールをチラ見せ。

roles/caddy/tasks/main.yml

---
- name: Install prerequisites
  ansible.builtin.apt:
    pkg:
      - debian-keyring
      - debian-archive-keyring
      - apt-transport-https
      - curl
      - gnupg
    update_cache: true
  become: true

- name: Add Caddy GPG key
  ansible.builtin.get_url:
    url: https://dl.cloudsmith.io/public/caddy/stable/gpg.key
    dest: /etc/apt/keyrings/caddy.asc
    mode: "0644"
  become: true

- name: Add Caddy repository
  ansible.builtin.apt_repository:
    repo: >-
      deb
      [signed-by=/etc/apt/keyrings/caddy.asc]
      https://dl.cloudsmith.io/public/caddy/stable/deb/ubuntu
      {{ ansible_facts['distribution_release'] }}
      main
    filename: caddy-stable
    state: present
  become: true

- name: Update apt cache after adding Caddy repo
  ansible.builtin.apt:
    update_cache: true
  become: true

- name: Install Caddy
  ansible.builtin.apt:
    name: "{{ caddy_package }}"
    state: present
  become: true

- name: Ensure Caddy log directory exists
  ansible.builtin.file:
    path: "{{ caddy_log_dir }}"
    state: directory
    owner: "{{ caddy_user | default('caddy') }}"
    group: "{{ caddy_user | default('caddy') }}"
    mode: "0755"
  become: true

- name: Render Caddyfile
  ansible.builtin.template:
    src: Caddyfile.j2
    dest: "{{ caddy_config_path }}"
    owner: root
    group: root
    mode: "0644"
  notify: Restart caddy
  become: true

- name: Ensure caddy service is enabled and started
  ansible.builtin.systemd:
    name: "{{ caddy_service }}"
    enabled: true
    state: started
  become: true

Caddyの設定自体はCaddyfileというものを書くわけだが、これが手頃に簡潔に記述できるのが非常に良い。

roles/caddy/templates/Caddyfile.j2

{% for site in caddy_sites %}
{{ site.domain }} {
  reverse_proxy {{ site.upstream }}

  log {
    output file {{ site.log_path | default(caddy_log_dir ~ '/' ~ site.domain ~ '.log') }} {
      roll_size 50MB
      roll_keep 5
      roll_keep_for 720h
    }
    format json
  }
}

{% endfor %}

Software Designの購読を始めた

SNSで技術ネタを掘るのにちょっと疲れてしまった今日このごろ。
編集さんが入っていて情報の質が、ある程度に担保された雑誌という権威(?)に回帰しようかなぁと考えた次第。

インターネットで熱量がある人を見つけるのが大変になった...ってその話は面倒臭いのでしません。

読んだ

SREの知識地図

常日頃として意識しておくとソフトウェアエンジニアとしての視座が少し高くなるハナシが詰まっていると思う。

愛するということ

家柄、家庭、幼少、ティーンエイジの環境ってその後の人生に相当の影響があるんだなって思った。悪く言えば呪いに近い。

昇降デスクを昇降できるようにした

ケーブルが干渉し合って上げ下げができなくなっていた。
1年くらい放ったらかしていたら座り過ぎで前立腺炎になった。
これは看過できない問題のため気合を入れて整理した。
ひたすらに長いケーブルを買い、上から下に垂らして、また上げると収まりがいい。

おわり

素振りは「そぶり」とも読むし、「すぶり」とも読む。

「Max for Live」のデバイスを開発する上でユーティリティクラスを作った

こんにちは!こんにちは!
FOLIO Advent Calendar 2025 6日目を担当する石川です。

厳しい寒さが続き、冬の陽だまりがことのほか暖かく感じる歳末の候、皆さまいかがお過ごしでしょうか。

これは何

Max for Liveでオリジナルの制作ワークフローを構築する楽しさを感じるここ数ヶ月です。
開発をする上で作ったユーティリティクラスを2つ紹介しています。

なんとなく対象読者

上記に該当せずとも、興味関心を持っていただけると幸いです。
よって「Ableton Live」と「Max for Live」を知らない方もいらっしゃると思いますので、簡単な説明も挟んでいます。
なお、パッチングやオブジェクトの細かい説明は割愛させていただきます。

Ableton Live(Live)」とは何か

端的に言えば音楽制作ソフトウェアです。
世の中にはCubase、Studio One、Logic Pro...といった選択肢もあるワケですが...
「Live」は名は体を表すようにライブ演奏やリアルタイム操作に強みがあります。

「Max for Live(M4L)」とは何か

Live内の機能を自作のプログラムで拡張できる仕組み、と言ったら言うべきか...。
専用のヴィジュアルプログラミング環境(Max)を用いて、エフェクターやオートメーションツールなどを開発できます。
他の開発者が配布しているプログラム(デバイス)も自身の環境にインストールして使用することもできます。

開発スタイル

基本的にはヴィジュアルプログラミングのような形式で開発を進めていきます。
画面上で「オブジェクト」と呼ばれる部品を配置・配線する形になります。
これを「パッチング」と呼ばれていたり、呼ばれていなかったりします。(曖昧)

Maxの独特のクセに このパッチングに慣れてしまえば多くの処理は視覚的かつ直感的に組めますし、コードを書くよりも手っ取り早く済む面もあるでしょう。
が、ありがたいことにJavaScript(V8エンジン)も搭載されているようです。
これによってテキストベースのプログラミングに馴染み深い私の土俵に引っ張り出すことができるワケです。

パッチ上のダイアルやメニューの値をJavaScript側で受け取り、Liveに命令を送るといったことが可能です。
パッチで完結するケースと、JavaScriptで書いたほうがラクできるケースを適宜使い分けていきましょう。

このあたりの棲み分けというか、設計方針に関しても論ずる点が割とあるのですが話が長くなりそうなので省略します。

live.thisdevice について(補足)

live.thisdevice reports three pieces of information about your Max Device. A bang message is automatically sent from the left outlet when the Max Device is opened and completely initialized, or when the containing patcher is part of another file that is opened. Additionally, a bang will be reported every time a new preset is loaded or the device is saved (and thus reloaded within the Live application). A 1 or 0 will be sent from the middle outlet when the Device is enabled or disabled, respectively. A 1 or 0 will be sent from the right outlet when preview mode for the Device is enabled or disabled, respectively. Used within Max, live.thisdevice functions essentially like the loadbang object. The middle and right outlets are inactive in this case.

https://docs.cycling74.com/reference/live.thisdevice より引用。

ひとまずこのようにパッチングをしておきましょう。
ちなみに左側はパッチング画面、右側はコンソール(ログ)画面です。

main.js の内容は以下のとおりです。

function bang() {
  post("Device initialization complete! \n");
}

このオブジェクトを使用することでデバイスが確実に初期化(読み込まれた)ことを担保することができます。
完全に初期化されていない状態で、LiveAPIやその他の処理を行うとエラーが起きがちです。
live.thisdevice からの通知を受け取ってから処理を実行することがセオリーと思います。

post関数

上述のソースコードを読んでみると不思議な post 関数が出てきました。

Prints a representation of the arguments in the Max window.

If post() has no arguments, it prints starting on the next line. Otherwise it prints the input on the current line separated by spaces. Arrays are unrolled to one level as with jsthis.outlet().

post - Max JS API | Cycling '74 Documentation より引用。

JavaScriptでは見慣れた console.log() を使いたくなるところですが...Maxでは機能しません!
Maxでは post 関数を使うことでコンソールへ文字列を出力することができます。

出力する毎に改行を挟みたいので、なんとなくさっくりと以下の関数を用意してしまうのも手でしょう。

function lnPost(obj) {
  if (obj) {
    post(obj + "\n");
  }
}

ロガー

...という話を踏まえた上で今回は人間に優しい形でログを出力するユーティリティクラスを実装しました。
形としてはよく見かける実装ではないでしょうか。

ログの出力例

v8: 2025-11-29T14:56:26.144Z [INFO] main: Device initialization complete!  

開発中のデバッグに何かと便利と思います。 JSON形式での出力やログレベルの設定といった味変はおまかせします。

「Live API」について

M4Lには、Liveの内部状態の取得および操作するためのクラスとして Live API が提供されています。
これによってトラックやエフェクターのパラメーターなどにアクセスし、プログラマブルに操作することが可能です。

LiveAPI - Max JS API | Cycling '74 Documentation

Liveの内部データ構造は階層構造となっており、これを「Live Object Model(LOM)」と定義されています。

Live Object Model | Cycling '74 Documentation より引用

プロジェクト全体(live_set)の中にトラック(tracks)があり、その子供としてデバイスエフェクターなど)、デバイスのパラメーター...などとぶら下がっている具合です。

各オブジェクトにアクセスする際は、文字列でパスを指定するような形となっています。
たとえば、プロジェクトの任意のトラックにアクセスする場合は、live_set tracks 0のようなパス指定します。
(初めてドキュメントを見たときにAPIの作りとしてかなり違和感がありましたが一旦は飲み込むこととしました。)

const track = new LiveAPI("live_set tracks 0");

コンストラクタにはパス以外にもコールバックを設定することができます。
基本的には子オブジェクトに変化(トラックの追加やパラメーターの変更)を監視することを目的としています。

property string The observed property, child or child-list of the object at the current path, if desired

For instance, if the LiveAPI object refers to "live_set tracks 1", setting the property to "mute" would cause changes to the "mute" property of the 2nd track to be reported to the callback function defined in the LiveAPI Constructor.

LiveAPI - Max JS API | Cycling '74 Documentation より引用

const liveSet = new LiveAPI(() => {
  logger.info("callback invoked!");
}, "live_set");

liveSet.property = "tracks"

このコールバックは Live APIインスタンス化された場合と property に設定したトラック(tracks)に変化があった場合に呼び出されます。

「Live API」のコールバックの挙動が不思議だった

さて、先述したようにこのコールバックについては以下の通りであるわけですが...

a function to be called when the LiveAPI object refers to a new object in Live (if the LiveAPI object's path changes, for instance) or when an observed property changes

LiveAPI - Max JS API | Cycling '74 Documentation より引用

なんとなく、監視対象のオブジェクトに変化があった場合にのみ処理を行いたい気持ちが出てくるわけです。
順当に初期化済みフラグを立てて条件分岐で早期returnすると良さそうに思います。

const logger = new Logger("init");
let initialized = false;

const liveApi = new LiveAPI(() => {
  logger.info("LiveAPI callback invoked");

  // 初回呼び出しではこの条件にマッチする
  if (!initialized) {
    // フラグを立てる
    initialized = true;
    logger.info("Initialization complete");

    // 監視対象のオブジェクトを設定する
    liveApi.property = "tracks";
    logger.info("Setting property to tracks");

    return;
  }

  logger.info("Detected change in live_set tracks");
}, "live_set tracks");

これでトラックが追加・削除といった変化があった場合にのみ、処理を継続することが期待できそうです。
が、実際に出力されたログは以下のとおりです。

v8: 2025-11-06T20:50:22.704Z [INFO] init: LiveAPI callback invoked  
v8: 2025-11-06T20:50:22.704Z [INFO] init: Initialization complete <- 初期化フラグが立った(期待した動作)
v8: 2025-11-06T20:50:22.704Z [INFO] init: LiveAPI callback invoked  

v8: 2025-11-06T20:50:22.704Z [INFO] init: Detected change in live_set tracks  <- トラックの変更が行われていないもかかわらず、変更が検知された(期待しない動作)
v8: 2025-11-06T20:50:22.705Z [INFO] init: LiveAPI callback invoked  

v8: 2025-11-06T20:50:22.705Z [INFO] init: Detected change in live_set tracks  <- 再びトラックの変更が行われていないもかかわらず、変更が検知された(期待しない動作)

v8: 2025-11-06T20:50:22.705Z [INFO] init: Setting property to tracks  <- 監視対象のオブジェクトを設定された

--- 手動でトラックを追加した場合にのみハンドラが呼び出されている ----

v8: 2025-11-06T20:50:37.048Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-06T20:50:37.048Z [INFO] init: Detected change in live_set tracks

監視対象のオブジェクトの設定が設定される前に、トラックの操作をしていないにも関わらず変更が検知される不思議な挙動に遭遇しました。
プロパティが更新されたあともコールバックが呼ばれ、プロパティを設定した時点では initialized フラグが true となり変更が検知されたかのような挙動になっている気がします。
というわけで初期化フラグに加えて、プロパティ設定済みフラグも用意してみることにします。

const logger = new Logger("init");

let initialized = false;
let detectedPropertySet = false;

const liveApi = new LiveAPI(() => {
  logger.info("LiveAPI callback invoked");

  // 初回呼び出しではこの条件にマッチする
  if (!initialized) {
    // フラグを立てる
    initialized = true;
    liveApi.property = "tracks";

    logger.info("Initialization complete & Property set to tracks");
    return;
  }

  // プロパティが設定されたタイミングではこの条件にマッチする
  if (!detectedPropertySet) {
    // フラグを立てる
    detectedPropertySet = true;
    logger.info("Detected property set to tracks");

    return;
  }

  logger.info("Detected change in live_set tracks");
}, "live_set");

出力されたログは以下のとおりです。

v8: 2025-11-09T04:12:21.709Z [INFO] init: LiveAPI callback invoked  
v8: 2025-11-09T04:12:21.709Z [INFO] init: LiveAPI callback invoked  
v8: 2025-11-09T04:12:21.709Z [INFO] init: Detected property set to tracks
v8: 2025-11-09T04:12:21.709Z [INFO] init: LiveAPI callback invoked  
v8: 2025-11-09T04:12:21.709Z [INFO] init: tracks changed
v8: 2025-11-09T04:12:21.709Z [INFO] init: Initialization complete & Property set to tracks 

またしても、監視対象のオブジェクトを触っていないにもかかわらず変更が検知されているように見えます。
プロパティが設定されたあともコールバックが呼ばれるのでしょうか?
さらにフラグを足してみます。

const logger = new Logger("init");

let initialized = false;
let detectedPropertySet = false;
let detectedFirst = false;

const liveApi = new LiveAPI(() => {
  logger.info("LiveAPI callback invoked");

  // 初回呼び出しではこの条件にマッチする
  if (!initialized) {
    // フラグを立てる
    initialized = true;
    liveApi.property = "tracks";

    logger.info("Initialization complete & Property set to tracks");
    return;
  }

  // プロパティが設定されたタイミングではこの条件にマッチする
  if (!detectedPropertySet) {
    // フラグを立てる
    detectedPropertySet = true;
    logger.info("Detected property set to tracks");

    return;
  }

  // とりあえず一発(?)実行されたタイミングではこの条件にマッチする
  if (!detectedFirst) {
    // フラグを立てる
    detectedFirst = true;
    logger.info("Detected first callback after property set");

    return;
  }

  logger.info("Detected change in live_set tracks");
}, "live_set");

ログの出力は以下の通り

v8: 2025-11-09T04:22:47.803Z [INFO] init: LiveAPI callback invoked  
v8: 2025-11-09T04:22:47.803Z [INFO] init: LiveAPI callback invoked  
v8: 2025-11-09T04:22:47.803Z [INFO] init: Detected property set to tracks  
v8: 2025-11-09T04:22:47.804Z [INFO] init: LiveAPI callback invoked  
v8: 2025-11-09T04:22:47.804Z [INFO] init: Detected first callback after property set
v8: 2025-11-09T04:22:47.804Z [INFO] init: Initialization complete & Property set to tracks

--- 手動でトラックを追加した場合にのみハンドラが呼び出されている ----

v8: 2025-11-09T04:25:46.433Z [INFO] init: LiveAPI callback invoked  
v8: 2025-11-09T04:25:46.433Z [INFO] init: Detected change in live_set tracks

それとなく当初の目的は達成できたような気がします。
これをうまくラップしたクラスを実装してみました。

ログ出力は以下の通り

v8: 2025-11-29T18:02:08.331Z [INFO] main: Device initialization complete!  
v8: 2025-11-29T18:02:08.332Z [INFO] LiveObjectObserver: LiveAPI initialized  
v8: 2025-11-29T18:02:08.332Z [INFO] LiveObjectObserver: Detected property set  
v8: 2025-11-29T18:02:08.332Z [INFO] LiveObjectObserver: Detected first change notification  
v8: 2025-11-29T18:02:08.332Z [INFO] LiveObjectObserver: Set property  [property=tracks] 

--- 手動でトラックを追加した場合にのみハンドラが呼び出されている ---

v8: 2025-11-29T18:02:13.937Z [INFO] main: Tracks changed  
v8: 2025-11-29T18:02:15.321Z [INFO] main: Tracks changed  

変更自体は検知できますが、状態の差分はLiveAPIでは提供されていません。
必要に応じて状態と差分を管理するロジックを実装すると良いでしょう。

しかし...このLiveAPIのコールバックの挙動に半月ほど悩まされました~...

おわりに

何かと想定と異なる挙動に何かと遭遇する場面が多々ありつつ、良くも悪くもなんとか慣れてきた今日この頃です。
こじんまりとしたプラクティスでしたが、同じ轍を踏まないことを祈り記事にした次第です。

それでは良いお年を。

追伸

株式会社FOLIOでは現在、メンバーを募集中です!
会社の雰囲気や仕事の様子など、カジュアル面談でぜひお話ししましょう。

カジュアル面談(社員紹介専用フォーム/社員紹介の方のみこちらからご応募下さい) - 株式会社FOLIO

追追伸

楽器の経験がある方や現在進行形で音楽活動をしている方など、音楽に親しみがあるメンバーが何気に多い会社に思います。
音楽性は違えど 互いを尊重し、各々の専門領域を活かしながら、妥協ない仕事ができる環境です。

会社の雰囲気や仕事の様子など、カジュアル面談でぜひお話ししましょう。(大事なことは2回言うタイプ)

カジュアル面談(社員紹介専用フォーム/社員紹介の方のみこちらからご応募下さい) - 株式会社FOLIO

「北へ。~White Illumination~」感想

残暑が続く今日この頃、皆さまいかがお過ごしでしょうか。

これは何

この記事は「北へ。~White Illumination~」というゲームについての感想です。

ジャンルとしては、「恋愛シミュレーションゲーム」に分類されると思います。
ひとまずメインヒロインの春野琴梨さんのルートにおいて、見るべきものは一通り見たような気がしています。
現状、かなり「食らって」いて、気持ちが冷めないうちに、気持ちに整理をつけるために、感想を書き留めておこうと思った次第です。

なお、この記事にはネタバレが含まれます。読まれる方はご留意ください。

ゲームついて

あらすじ

夏休みを利用し、14日間の北海道旅行へ出かける高校2年生の少年。
親戚である春野琴梨(はるのことり)の案内で札幌や小樽など、実在する北海道の観光地をめぐり、8人の女の子たちに出会い、夢や悩みなど、彼女たちとの会話を通して恋をしている自分に気付く。
そして冬休み、少年は想いを伝えるために再び北海道へ。
札幌・大通公園で開催される「ホワイトイルミネーション」の街路樹のもとで、いよいよ感動のクライマックスへ…。

北へ。 - 株式会社レッド・エンタテインメント より引用)

制作背景

本作は同時期に行われていた北海道活性化キャンペーン「MOVE ON北海道=北からの声かけ運動」に協賛しており、北海道の各機関からの協力を得ているのが特徴である。実在するお店やレストランなどが実名で登場する。北海道に到着した日に、琴梨からガイドブックを渡される。この本に本作のロケーション撮影に使用した場所も含めて、各デートスポットの細かい紹介が載っている。

北へ。- Wikipedia より引用)

参考までに以下は実際のゲームのスクリーンショットです。
背景には、北海道札幌市にある「さっぽろテレビ塔」が写っています。

プレイした理由

ようこそ札幌へ。私 この街が大好きなの。
だからお兄ちゃんも 気に入ってくれると嬉しいな

シンプルながら、この一言がやけに刺さってしまいました。
ファースト・インプレッションとしては「羨ましい」に近い感情だったのかもしれません。

私の出身は山形県です。
地元のことは悪い街だとは思っていませんが、際立って良いと言えるものも思い当たらない「パッとしない街」と思っています。
県外から遊びに来た方々が魅力に思う点はいくつかあるようで、そう思ってもらえること自体は嬉しいと感じます。
とはいえ、「この街が大好き」と胸を張って言うことはできないでしょう。
(もちろん、良いところを知っている人もいれば、もっと良い街にしていこうと努力している人もいます。そうした彼らを否定するつもりはないです。)

ゲームを通して表現される登場人物たちの地元愛は、「北海道を魅力的に描くこと」を前提とした制作背景があるわけですが......
それを踏まえた上でも「この街が大好きなの」というセリフには、不思議な引力と熱がありました。
私がこのゲームを遊んでみよう思ったのは、彼女のその一言にほとんど集約されています。

プレイ環境の導入

このゲームが対応しているのは、ドリームキャストのみのようです。
移植もリメイクもされておらず、現況のゲーム機やPCでは遊ぶことができないようです。
2025年に今さらドリームキャストを購入することにはやや覚悟が必要で、当初は画集だけ買ってお茶を濁していました。
が、やはり我慢ならずヤフオク!で本体を落札。ソフトはAmazonにて購入しました。

昔なつかし3色ケーブルによる映像・音声出力だったため、HDMIコンバーターも合わせて用意しました。

全体的な感想

まずは、ゲームを遊ぶきっかけとなった春野琴梨さんのルートを走りました。
一緒に観光地を巡るなかで距離が縮まり、最終的には主人公と結ばれ、ダイビングライセンスを取得して念願のナポレオンフィッシュにも出会って......という爽やかなエンディングでした。
ですが、ゲーム機の電源を切ればすべてが終わってしまう別れというべきか、現実に引き戻された瞬間には強い喪失感を感じました。

旅行とか、人と会って楽しかったときの帰り道とか...強い余韻といえばそれまでですね。
それほどの余韻を残す感情移入や没入感を得られた仕組みや理由は、説明できる範囲で大きく分けて2つあると思いました。

グラフィック

20年以上も前のゲーム機ということもあり、技術的な制約から解像度や描画の粗さは否めません。
ですが、そのぶん想像力が入り込む余地がありましたし、むしろそれゆえに「居場所がある」かのような錯覚すら呼び起こしました。
また、前述のとおり背景画像に実写の写真が使われている点も、没入感を後押ししていたように思います。

コミュニケーション・ブレイク・システム

会話の途中にタイミングよくボタンを押すことで、こちらから返事をすることができるシステムです。
逆にこれを積極的に行わないと好感度を稼ぐことができません。
ゲームでありながら、会話のラリーが自然と続いているかのような感覚を与えてくれます。
調べたところは賛否両論のあるシステムのようです。
私にとってはとても心地よく、ゲームの世界に集中できる要素でした。

怪文書

ここからは文脈の説明がない、わかる人にわかったらよい感想を雑多に書いておきます。
ハッとするような共感を呼び起こす言葉が多く散りばめられていて、良いゲームだなぁ...とつくづく思いました。

春野琴梨さんについて

その1

出会ったから 寂しいのかな
それとも、出会う前からずっと寂しかったのかな

もともと無意識のうちにずっと「寂しい」があった。いや、与えられている。
主人公とのやりとりや距離感の変化がトリガーとして、それをようやく自覚(意識)するようになったのかなぁと考えていました。

名前重要!!」、プログラマの矜持としてそう思います。
あらゆる感情や関係性は名前をつけるまで存在しないし、機能しない。

その2

人間は手も足もあって
その分 いろんなことが出来るんだけど...
だからこそ 何をやっていいのかわかんなくなっちゃって
ついつい悩んじゃうような そんな気がするの..
それで お魚より自由なはずの人間が
お魚よりも不自由になっているって言うか...

おっしゃるとおりでございます。
魚は泳ぐことを目的に最適化され、それを疑う余地がなく生きています。
一方、人間は自由と引き換えに不安と迷いを背負わされて生きる宿命にありますね。
琴梨さんは、お魚さんみたいにまっすぐな信念を貫いて生きてほしいな...と思わずにはいられませんでした。

立ち止まって考えてはたどたどしくも言葉にして口にするところに魅力を感じます。
難しいようでそんなカンタンなことができないばかりの人生です。

愛田めぐみさんルートについて

物語の中盤に親戚の牧場が人手不足で手伝いに行く分岐が存在します。
「困っているなら手伝いに行くのが筋だろう」と良心にしたがって選択を選んだところ──
あれよあれよという間に、めぐみさんルートに直行してしまいました。

手伝いを終え、別れの電車で窓越しに指で「だいすき」と書かれたときには......
こんな健気な子を差し置いて、冬編でめぐみさん以外を選ぶことはできませんでした。

牧場の風景も、移住にまつわるエピソードも、良いシナリオだったと思います。
とはいえ、こまめにセーブしておいて本当に良かったです。変な勘が働いていました。

おわりに

プレイの動機はともかくとして、ついにアラサーに突入した人間が「ゲームで青春してみよう!」という構図には変わりがなく正直マズいとは思いました。
実際、同い年の友人からも「極まっている」とのお言葉をいただきました。
しかしながら、ここまで心を揺さぶられるゲーム体験はかなり久しいものでした。
遊んで良かったです!

恋、やりて〜。

「恋愛なんて興味ない」ってスカして生きてきました。
そうした感情を引きずり出してくれたこのゲームに感謝しています。
もっと早く出会いたいゲームだったかな?

北へ 行こう ランララン...🎵

追伸

せっかくドリームキャストを買ったので、他のゲームもいろいろ遊んでみたくなってきました。
おすすめのゲームがあれば、教えていただけると嬉しいです。

Scalaのアンダースコアのパターン整理

背景

これは何

Scalaのアンダースコアは文脈によって意味が異なる。
この記事は初心者向けにパターンを整理したもの。
(抜け漏れ・呼び方が異なっているなどあったらすみません。)

匿名関数の引数

引数を名前なしで扱う。

// List(1, 2, 3).map(x => x * 2) と同じ意味
List(1, 2, 3).map(_ * 2)

関数の部分適用

「まだ決まっていない引数」を仮置きして関数の一部を先に指定する。

def add(a: Int, b: Int): Int = a + b
val f = add(_, 5)
f(3) // 8

高カインド型

型引数を取る型を引数として受け取る。

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

参考

使わない値を無視

パターンマッチやfor式などで使わない値を無視する。

val (_, y) = (1, 2)

for {
  _ <- f()
  _ <- g()
} yield ()

型引数のワイルドカード

受け取る型が何でもいい場合。

def f(xs: List[_]) = ...

Scala3では型引数のワイルドカード? になる。

def f(xs: List[?]) = ...

パターンマッチのワイルドカード

すべての値にマッチする場合。

hoge match {
  case "apple" => println("りんごだよ")
  case "orange" => println("みかんだよ")
  case _ => println("しらないよ")
}

ただし、実装・運用上の注意点がある。
(参考の動画を参照してください。)

参考

インポート文のワイルドカード (Scala2まで)

何らかのパッケージ内のすべてのメンバーをインポートする場合。

// mutable パッケージ以下を全部インポートする
import scala.collection.mutable._

Scala3からはインポート文のワイルドカードは * になる。

// mutable パッケージ以下を全部インポートする
import scala.collection.mutable.*

参考

初期値の設定

型によって定義されている初期値を代入する。

val a: Int = _ // 0
val b: Double = _ // 0.0
val c: String = _ // null

case class D()
val d: D = _ // null

Scala3ではこの機能は廃止されuninitializedが導入された。

import scala.compiletime.uninitialized

var x: A = uninitialized

参考

おわり

全部覚えてScalaマスターになろう!

謝辞

上記の方々のご意見を参考に記事を更新しました! ありがとうございます m(_ _)m

遷都して1年が経った

約1年前に山形県から東京都に引っ越した。(遷都した)
その間に感じたことや暮らしの変化を集中力が持つ限りざっくばらんに書いてみようと思った。

上から撮った渋谷の風景

部屋が狭い

少し想定外だったのは、借りた部屋の狭さだと思う。
コンピューターもあるし、楽器もあるし、あっという間に部屋の中は手狭になった。
家族や友人を呼ぶと、どこか申し訳ない気持ちになる。
かといって、都心で広い部屋を借りるのは今の経済状況では現実的ではない。
不動産も暴騰しているという話だし、都会で暮らす上でのわかりやすい洗礼のように思う。

道が狭い

運転どころか徒歩もすれ違う自転車と電動の何かしらの乗り物に恐怖するばかり。
意識しているのか無意識なのか、命がけだし命知らずな人が多い。

生活の利便性のいろいろ

言わずもがな移動は電車が中心になった。
山形に住んでいた頃と比べて、気軽にお酒が飲めるようになったのは少し嬉しい。
これまでは車なしには考えられない暮らしだったため、酒を飲むにしても帰りの心配がかなり大きかった。
それと、電車に乗っている間は本を読むことができるため、読書量は1ミリくらい増えた。

Uber EatsやAmazon Freshなどが使えるようになって、食事や買い物の自由度が増した。
動きたくない、動けないときに頼れる選択肢があると思うと、気持ちに余裕が生まれる。
そういった便利なインフラには時折助けられている。

自然

日々、コンクリートに埋もれて暮らしている。
ある日、高尾山へ登山へ行ったときに得た開放感。
それと対比して無意識のうちに日々の暮らしになんとなく息苦しさがあったことに気がついた。
空が広いだけで心が安らぐのか、地元に帰ったときも同じように感じた。
何かを引き換えに何かを手放しているような感覚だ。

人との関わり

人との関わりは自然と増えた。
興味のあるイベントに顔を出しやすくなり、その流れで交友関係も広がってきた。
元々、あまり人と積極的に関わる性格ではないが、単純に人の母数が違うなと思う。
孤独は感じることは少なくなったと思う。

新宿と高円寺によく遊びに行っている。
好きなクラブと好みの風合いの古着屋がある。
他にも好きな街はあるけど、頻繁に行くのはこの2つだと思う。

店をやっている人たちの話を聞いたり、暮らしぶりを垣間見たりしていると、自分の欲望とかワナビーが刺激される。
以前はぼんやりとしていた夢とか目標とかが輪郭を持ち始めた。
こういうふうに生きたいとか、このようにありたい、と思える瞬間が増えてきたのは、遷都して得られた気持ちの大きな変化だと思う。

おわり

まだ東京は探索しきれいていないし、やりたいことがたくさんある。
多分、歩けば歩くほど新しい発見があってキリがないのかもしれない。
特別な事情が起きない限りは、もう少し住んでみたいと考えている。

PlayFrameworkのWebSocket#acceptのimplicitパラメーターのMessageFlowTransformerについて

Websocket#accept ではimplicitパラメーターに MessageFlowTransformer を取っている。(URL)

def accept[In, Out](
    f: RequestHeader => Flow[In, Out, ?]
)(implicit transformer: MessageFlowTransformer[In, Out]): WebSocket = {
  acceptOrResult(f.andThen(flow => Future.successful(Right(flow))))
}

読んで字の如く、メッセージをトランスフォ〜ムしてくれるもので以下のようなトレイトになっている。(URL)

trait MessageFlowTransformer[+In, -Out] { self =>

  /**
    * Transform the flow of In/Out messages into a flow of WebSocket messages.
    */
  def transform(flow: Flow[In, Out, ?]): Flow[Message, Message, ?]

  /**
    * Contramap the out type of this transformer.
    */
  def contramap[NewOut](f: NewOut => Out): MessageFlowTransformer[In, NewOut] = { (flow: Flow[In, NewOut, ?]) =>
    self.transform(
      flow.map(f)
    )
  }

  /**
    * Map the in type of this transformer.
    */
  def map[NewIn](f: In => NewIn): MessageFlowTransformer[NewIn, Out] = { (flow: Flow[NewIn, Out, ?]) =>
    self.transform(
      Flow[In].map(f).via(flow)
    )
  }

  /**
    * Map the in type and contramap the out type of this transformer.
    */
  def map[NewIn, NewOut](f: In => NewIn, g: NewOut => Out): MessageFlowTransformer[NewIn, NewOut] = {
    (flow: Flow[NewIn, NewOut, ?]) =>
      {
        self.transform(
          Flow[In].map(f).via(flow).map(g)
        )
      }
  }
}

デフォルトでは、MessageFlowTransformer[String, String] とか MessageFlowTransformer[Array[Byte], Array[Byte]] とかが定義されていて(URL)、これのおかげで Websocket#accept[String, String] とかを書き始めることができる。
たとえば、circeでJSONをパースしたいな〜となったときはこのトレイトを実装するか、 MessageFlowTransformer#map で既存の実装を変換すると良い。

// エラー処理は後続でよしなに
implicit val messageFlowTransformer: MessageFlowTransformer[Either[Error, Json], Json] = {
  MessageFlowTransformer.stringMessageFlowTransformer.map(
    parser.parse(_),
    json => json.noSpaces
  )
}

これで Websocket#accept[Either[Error, Json], Json] が使えるようになるよ、という小ネタでした。
おわり。あけましておめでとうございました。今年もどうぞよろしくお願いします。

PlayFramework(Scala)における構造化ロギングとトレーシング

こんにちは!こんにちは!
こちらは FOLIO Advent Calendar 2024 9日目の記事です。
この記事では、PlayFrameworkでの構造化ロギングとトレーシングについて検証を行いましたので実例を交えて紹介します。

 前提条件

  • PlayFramework 3.0.5
  • Scala 3.3.3

サンプルプロジェクトはこちらです。
https://github.com/stoneream/play-logging-sandbox

構造化ロギングって何?

アプリケーションのログは単純なテキストで出力されていることがままあるかと思います。
以下はアクセスログの出力例です。(commit)

INFO  f.AccessLog - Request: POST /widgets
INFO  f.AccessLog - Response: POST /widgets (400)
INFO  f.AccessLog - Request: GET /assets/stylesheets/main.css
INFO  f.AccessLog - Request: GET /assets/javascripts/hello.js
INFO  f.AccessLog - Response: GET /assets/stylesheets/main.css (304)
INFO  f.AccessLog - Response: GET /assets/javascripts/hello.js (304)
INFO  f.AccessLog - Request: POST /widgets
INFO  f.AccessLog - Response: POST /widgets (303)
INFO  f.AccessLog - Request: GET /widgets
INFO  f.AccessLog - Response: GET /widgets (200)
...

これはこれで人の目にはわかりやすいのですが、いくつか課題があります。
ここ最近はDatadogやElasticsearchなどにログを集め、可視化や分析することが一般的になってきています。そうした中でフリーフォーマットのログから目的の情報を抽出するために、何らかの形に変換したりパーサーや正規表現を記述することがあります。
ですが、ログを出力する段階でJSONのような構造化された形式にしておくと変換が用意で扱いやすいよね、というのが構造化ロギングです。

Scalaで構造化ロギングをやりたい!

JavaScalaのロギングライブラリとして広く使われている Logback の場合、 logstash-logback-encoder を組み合わせることで簡単にJSON形式でログを出力することができます。

導入方法についてはリポジトリもしくはこちらの記事で詳しく解説されています。

以下は、logstash-logback-encoderを使ってログをJSON形式で出力した例です。(commit)
ログがJSON形式で各項目がフィールドに分かれていることがわかります。

{"@timestamp":"2024-11-07T22:55:43.195563898+09:00","@version":"1","message":"Slf4jLogger started","logger_name":"org.apache.pekko.event.slf4j.Slf4jLogger","thread_name":"application-pekko.actor.default-dispatcher-5","level":"INFO","level_value":20000}
{"@timestamp":"2024-11-07T22:55:43.249130565+09:00","@version":"1","message":"Application started (Dev) (no global state)","logger_name":"play.api.Play","thread_name":"play-dev-mode-pekko.actor.default-dispatcher-8","level":"INFO","level_value":20000}
{"@timestamp":"2024-11-07T22:55:43.293556771+09:00","@version":"1","message":"Request: GET /widgets","logger_name":"filters.AccessLog","thread_name":"application-pekko.actor.default-dispatcher-5","level":"INFO","level_value":20000}
{"@timestamp":"2024-11-07T22:55:43.382018947+09:00","@version":"1","message":"Response: GET /widgets (200)","logger_name":"filters.AccessLog","thread_name":"application-pekko.actor.default-dispatcher-8","level":"INFO","level_value":20000}
{"@timestamp":"2024-11-07T22:55:43.426615795+09:00","@version":"1","message":"Request: GET /assets/javascripts/hello.js","logger_name":"filters.AccessLog","thread_name":"application-pekko.actor.default-dispatcher-5","level":"INFO","level_value":20000}
...

さて、ログ中には単純なメッセージだけではなくプログラムの実行時の変数の内容を出力していることがあります。(前述のケースでは、Request: GET /widgets など)

logstash-logback-encoder では StructuredArguments を使うことでログにフィールドを追加しつつ、値を展開することができます。
以下は、HTTPメソッド名(method)とURI(uri)をフィールドに追加した例です。(commit)

{"@timestamp":"2024-11-07T23:08:29.675166652+09:00","@version":"1","message":"Request: method=GET uri=/assets/stylesheets/main.css","logger_name":"filters.AccessLog","thread_name":"application-pekko.actor.default-dispatcher-9","level":"INFO","level_value":20000,"method":"GET","uri":"/assets/stylesheets/main.css"}
{"@timestamp":"2024-11-07T23:08:29.675166702+09:00","@version":"1","message":"Request: method=GET uri=/assets/javascripts/hello.js","logger_name":"filters.AccessLog","thread_name":"application-pekko.actor.default-dispatcher-6","level":"INFO","level_value":20000,"method":"GET","uri":"/assets/javascripts/hello.js"}
{"@timestamp":"2024-11-07T23:08:29.709823419+09:00","@version":"1","message":"Response: method=GET uri=/assets/javascripts/hello.js (status=304)","logger_name":"filters.AccessLog","thread_name":"application-pekko.actor.default-dispatcher-12","level":"INFO","level_value":20000,"method":"GET","uri":"/assets/javascripts/hello.js","status":304}
{"@timestamp":"2024-11-07T23:08:29.709821669+09:00","@version":"1","message":"Response: method=GET uri=/assets/stylesheets/main.css (status=304)","logger_name":"filters.AccessLog","thread_name":"application-pekko.actor.default-dispatcher-13","level":"INFO","level_value":20000,"method":"GET","uri":"/assets/stylesheets/main.css","status":304}
{"@timestamp":"2024-11-07T23:08:29.79536314+09:00","@version":"1","message":"Request: method=GET uri=/assets/images/favicon.png","logger_name":"filters.AccessLog","thread_name":"application-pekko.actor.default-dispatcher-12","level":"INFO","level_value":20000,"method":"GET","uri":"/assets/images/favicon.png"}
...

ログのトレーシングとは?

プロダクション環境では大量のリクエストを捌くケースがあります。また、PlayFrameworkでは処理が非同期で行われています。
リクエストを受け付け、何らかの処理を行い、レスポンスを返すという一連の流れで出力されるログが入り乱れると、どの文脈で発生したログであるかを追いかけるることは困難です。
このような場合にリクエストごとに一意なIDを付与し、同じ文脈で発生したログにはそのIDを含めることで解決できます。

Scalaでログのトレーシングをやりたい!

Logbackを使用している場合は、MDC(Mapped Diagnostic Context)という機能を使うことが一般的です。 しかし、MDCはスレッドごとに値を保持するため、スレッドを細かく切り替えながら処理を行うPlayFrameworkではひと工夫必要です。

以下は、リクエストを受け付けたタイミングでセットしたID(traceId)がレスポンスを返すタイミングで出力したログで出力されていない様子です。thread_nameからスレッドが切り替わっている様子もわかります。(commit)

{"@timestamp":"2024-11-11T23:51:21.247108788+09:00","@version":"1","message":"Request: method=GET, uri=/widgets","logger_name":"action.TraceableLoggingAction","thread_name":"application-pekko.actor.default-dispatcher-4","level":"INFO","level_value":20000,"traceId":"jfeK57QJ","method":"GET","uri":"/widgets"}
{"@timestamp":"2024-11-11T23:51:21.252257058+09:00","@version":"1","message":"Response: method=GET, uri=/widgets (status=200)","logger_name":"action.TraceableLoggingAction","thread_name":"application-pekko.actor.default-dispatcher-7","level":"INFO","level_value":20000,"method":"GET","uri":"/widgets","status":200}

ログを出力するたびにMDCに値をセットする処理やトレース用のIDをコード中で引き回し続ける処理を書くのはやや大変です。
以下の記事を参考にしながら、スレッドが切り替わる際にMDCの値を引き継ぐような仕組みを作ります。

また、ExecutorServiceConfigurator を実装すると自前で用意したスレッドプールも設定ファイル上で指定して使えるようになります。
以下は、実装とログの出力例です。スレッドプールが切り替わってもトレースIDが引き継がれていることがわかります。(commit)

{"@timestamp":"2024-11-12T01:09:19.910970744+09:00","@version":"1","message":"Application started (Dev) (no global state)","logger_name":"play.api.Play","thread_name":"play-dev-mode-pekko.actor.default-dispatcher-7","level":"INFO","level_value":20000}
{"@timestamp":"2024-11-12T01:09:19.986438596+09:00","@version":"1","message":"Request: method=GET, uri=/widgets","logger_name":"actions.TraceableLoggingAction","thread_name":"pool-189-thread-5","level":"INFO","level_value":20000,"traceId":"5zOBLNHL","method":"GET","uri":"/widgets"}
{"@timestamp":"2024-11-12T01:09:20.041030994+09:00","@version":"1","message":"Response: method=GET, uri=/widgets (status=200)","logger_name":"actions.TraceableLoggingAction","thread_name":"pool-189-thread-6","level":"INFO","level_value":20000,"traceId":"5zOBLNHL","method":"GET","uri":"/widgets","status":200}
{"@timestamp":"2024-11-12T01:09:40.674878885+09:00","@version":"1","message":"Request: method=POST, uri=/widgets","logger_name":"actions.TraceableLoggingAction","thread_name":"pool-189-thread-3","level":"INFO","level_value":20000,"traceId":"4l1jg2Vl","method":"POST","uri":"/widgets"}
{"@timestamp":"2024-11-12T01:09:40.678931201+09:00","@version":"1","message":"Widget added name=YOYO, price=1234","logger_name":"controllers.WidgetController","thread_name":"pool-189-thread-3","level":"INFO","level_value":20000,"traceId":"4l1jg2Vl","name":"YOYO","price":1234}
{"@timestamp":"2024-11-12T01:09:40.679341438+09:00","@version":"1","message":"Response: method=POST, uri=/widgets (status=303)","logger_name":"actions.TraceableLoggingAction","thread_name":"pool-189-thread-3","level":"INFO","level_value":20000,"traceId":"4l1jg2Vl","method":"POST","uri":"/widgets","status":303}
...

おわり

上記の実装では、他のMDCを伝搬する仕組みを持たないスレッドプールには値を引き継ぐことができない問題が残ります。
以前は、Kamonというライブラリを使用していたのですが、最新のPlayFrameworkに対応していない(パッと見で対応方法もよくわからなかった;;)ため、色々とやり方を検証していた次第でした...。 もっと良い方法があるよ!という方はぜひ教えていただけると幸いです。