無駄と文化

実用的ブログ

守破離の先に在るもの

「守破離」という言葉がある。 型やルールを身につける「守」、状況に応じてあえて崩す「破」、そして型の存在を意識せず自然に動ける「離」。
守破離を大事にする人は多いけど、守破離を徹底した先に何が在るのかイメージできてる?と気になったりします。

先に結論から。「守破離」の先に在るものは「くつろぎ」です。

 

茶会に例える

「守破離」は茶道の世界からきた言葉だそうなので茶会に例えてみる。

茶会の目的は、客人をもてなし、一体感を味わうことです。
しかし、全員が好き勝手に振る舞えば場は混乱し、誰も落ち着けません。
そこでまずはルールやマナーを全員が守る。これが「守」にあたります。

とはいえ、守ることに意識が向きすぎると、その場で起こる出来事を十分に楽しめなくなります。
そこでその場に応じてルールを緩めることも大事になります。「正座がつらければあぐらで構わない」「お茶が苦手なら香りだけ楽しんでもよろしい」などなど。これが「破」です。

そうすると全員が余裕を持って場に臨めるようになります。 ルールの存在を意識せずとも場は調和し、自然にくつろげるようになります。これが「離」です。

 

なぜくつろぎが必要か

力を発揮するには、リラックスして構えなければいけないから。

身体がこわばっていては大きな力は出せず、かといって完全に脱力していてもいざという時に反応できません。
その場の成果を最大化するには、意識を向けながらも肩の力を抜く状態が理想。

 

習熟プロセスか、くつろぎか

こんなこと言ってると「守破離って習熟プロセスなんじゃないの?」とか言われそう。

思うに、「守破離」は個人にとっては習熟の道筋であり、集団にとっては成果を最大化するための方法論なんだと思う。

「自分は駆け出しだからまずは基本を守ろう」それもいいことだ。同時に、行き着く先に「くつろぎ」があるのをイメージしておきたい。 そして、力みすぎて身体がこわばっていないか気にかけてみてほしい。

 

 

私からは以上です。

Hono GraphQL Server Middleware のサンプルコード

Web アプリケーションフレームワークの Hono にはさまざまなミドルウェアが提供されています。その一つが Hono GraphQL Server Middleware で、Hono をベースとした GraphQL サーバーを構築できます。

www.npmjs.com

「よし Hono で GraphQL サーバー書くぞ!」という人のために公式の README にサンプルコードが用意されているのですが。これが本当に最小限で、初見で走らせてみるのは情報不足と感じます。そこでもう一歩踏み込んだ実践的なサンプルコードを用意しました。

github.com

ここで動作している様子も見られます。

hono-graphql-server-example.todays-mitsui.workers.dev

この記事ではサンプルコードを解説します。

 

目次

 

まずはローカルで動かそう

Hono GraphQL Server Middleware は "Server" と名前に冠していますがサーバーとしての機能がありません。まずはローカルで動かしてリクエストを送ってみたいところですが公式のサンプルコードにはローカルで動かす方法は書かれていません。

// index.ts

import { Hono } from 'hono'
import { type RootResolver, graphqlServer } from '@hono/graphql-server'
import { buildSchema } from 'graphql'

export const app = new Hono()

const schema = buildSchema(`
type Query {
  hello: String
}
`)

const rootResolver: RootResolver = (c) => {
  return {
    hello: () => 'Hello Hono!',
  }
}

app.use(
  '/graphql',
  graphqlServer({
    schema,
    rootResolver,
    graphiql: true,
  })
)

app.fire()

これをそのまま tsx コマンド で実行しようとしてもエラーになります。

$  tsx index.ts 
file:///Users/todays_mitsui/oss/hono-graphql-server-example/node_modules/hono/dist/hono-base.js:224
    addEventListener("fetch", (event) => {
    ^

ReferenceError: addEventListener is not defined
    at Hono.fire (file:///Users/todays_mitsui/oss/hono-graphql-server-example/node_modules/hono/dist/hono-base.js:224:5)
    at <anonymous> (/Users/todays_mitsui/oss/hono-graphql-server-example/index.ts:28:5)
    at ModuleJob.run (node:internal/modules/esm/module_job:274:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)

Node.js v23.11.0

 

サンプルコードをローカルで動かす方法はいくつかあります。
@hono/node-server を使う方法と wrangler コマンドを使う方法です。それぞれ見てみましょう。

 

@hono/node-server を使う方法

@hono/node-server をインストールしてから import します。

// index.ts

+ import { serve } from '@hono/node-server'
  import { Hono } from 'hono'
  import { type RootResolver, graphqlServer } from '@hono/graphql-server'
  import { buildSchema } from 'graphql'

  const app = new Hono()

  const schema = buildSchema(`
  type Query {
    hello: String
  }
  `)

  const rootResolver: RootResolver = (c) => {
    return {
      hello: () => 'Hello Hono!',
    }
  }

  app.use(
    '/graphql',
    graphqlServer({
      schema,
      rootResolver,
      graphiql: true,
    })
  )

- app.fire()
+ serve(app, (info) => {
+   console.log(`Listening on http://localhost:${info.port}/graphql`)
+ })

するとこのコードを実行するだけでローカルサーバーが立ち上がるようになります。

$ tsx index.ts
Listening on http://localhost:3000/graphql

ローカルで GraphiQL が動いている

 

wrangler コマンドを使う方法

Hono がデフォルトで Web サーバーとしての機能を持っていないのは、Claoudflare Workers などの環境で Service Worker として動作することを第一に設計されているからです。そのために Hono も Hono GraphQL Server もデフォルトではアプリケーションサーバーとして動作するようにはなっていません。

公式のサンプルコードはローカルで動かせる形にはなっていませんが、代わりに Claoudflare Workers にそのままデプロイ可能な形になっています。そこで Cloudflare 公式が提供している wrangler コマンド を使いましょう。wrangler コマンドにはコードをローカルで実行してプレビューする機能もあります。

$ npm install --save-dev wrangler

そして npm scripts としてプレビュー用のコマンドを書きます。

// package.json

{
  "scripts": {
    "dev": "wrangler dev --port=3000 index.ts"
  },
  "dependencies": {
    "@hono/graphql-server": "^0.5.1",
    "hono": "^4.7.7"
  },
  "devDependencies": {
    "wrangler": "^4.12.0"
  }
}

TypeScript コードはサンプルそのままです。

// index.ts

import { Hono } from 'hono'
import { type RootResolver, graphqlServer } from '@hono/graphql-server'
import { buildSchema } from 'graphql'

export const app = new Hono()

const schema = buildSchema(`
type Query {
  hello: String
}
`)

const rootResolver: RootResolver = (c) => {
  return {
    hello: () => 'Hello Hono!',
  }
}

app.use(
  '/graphql',
  graphqlServer({
    schema,
    rootResolver,
    graphiql: true,
  })
)

app.fire()

 

npm run dev でローカルサーバーが立ち上がります。

$ npm run dev

> dev
> wrangler dev --port=3000 index.ts

 ⛅️ wrangler 4.14.1
-------------------

▲ [WARNING] The entrypoint index.ts has exports like an ES Module, but hasn't defined a default export like a module worker normally would. Building the worker using "service-worker" format...

No bindings found.
⎔ Starting local server...
[wrangler:inf] Ready on http://localhost:3000

おっと何やら WARNING が出ていますね。修正しましょう。

// index.ts

  import { Hono } from 'hono'
  import { type RootResolver, graphqlServer } from '@hono/graphql-server'
  import { buildSchema } from 'graphql'

- export const app = new Hono()
+ const app = new Hono()

  const schema = buildSchema(`
  type Query {
    hello: String
  }
  `)

  const rootResolver: RootResolver = (c) => {
    return {
      hello: () => 'Hello Hono!',
    }
  }

  app.use(
    '/graphql',
    graphqlServer({
      schema,
      rootResolver,
      graphiql: true,
    })
  )

- app.fire()
+ export default app

app.fire() を削除し app 自体を default export するように変更しました。
再度 npm run dev すると WARNING 無しでローカルサーバーが立ち上がります。

$ npm run dev

> dev
> wrangler dev --port=3000 index.ts

 ⛅️ wrangler 4.14.1
-------------------

No bindings found.
⎔ Starting local server...
[wrangler:inf] Ready on http://localhost:3000

ローカルで GraphiQL が動いている

 

リゾルバを追加定義してみる

公式ドキュメントには定数として 'Hello Hono!' を返すだけのやる気のないリゾルバしか定義されていません。

const rootResolver: RootResolver = (c) => {
  return {
    hello: () => 'Hello Hono!',
  }
}

もう少し実用的なリゾルバを定義してみましょう。
結果だけ先に見たい人のために完成品のコードがここにあります。

github.com

 

スキーマとデータ

リゾルバを実装する前にスキーマを定義しておきます。

const typeDefs = buildSchema(`
type Author {
  id: Int!
  name: String!
  firstName: String!
  lastName: String!
  books(findTitle: String): [Book!]!
}

type Book {
  id: Int!
  title: String!
  author: Author
}

type Query {
  hello(name: String): String!
  book(title: String!): Book
  books: [Book!]!
}
`);

書籍と作者を検索できる感じにしました。
Book.author で書籍から作者を参照できます。 Author.books で作者から書籍の一覧を参照できます。

あくまでも例なのでデータベースなど用意せずコード中に直接書いたデータを返すようにします。[src/graphql/data.ts https://github.com/todays-mitsui/hono-graphql-server-example/blob/main/src/graphql/data.ts] にサンプルデータを置いて使います。

// src/graphql/data.ts

export const authors: Author[] = [
    { id: 0, lastName: "夏目", firstName: "漱石" },
    { id: 1, lastName: "芥川", firstName: "龍之介" },
    { id: 2, lastName: "太宰", firstName: "治" },
    { id: 3, lastName: "川端", firstName: "康成" },
    { id: 4, lastName: "宮沢", firstName: "賢治" },
    { id: 5, lastName: "森", firstName: "鷗外" },
];

export const books: Book[] = [
    { id: 0, title: "吾輩は猫である", authorId: 0 },
    { id: 1, title: "こころ", authorId: 0 },
    { id: 2, title: "羅生門", authorId: 1 },
    { id: 3, title: "人間失格", authorId: 2 },
    { id: 4, title: "斜陽", authorId: 2 },
    { id: 5, title: "走れメロス", authorId: 2 },
    { id: 6, title: "雪国", authorId: 3 },
    { id: 7, title: "銀河鉄道の夜", authorId: 4 },
    { id: 8, title: "舞姫", authorId: 5 },
    { id: 9, title: "伊勢物語", authorId: null },
];

 

リゾルバ: 書籍検索

書籍名を指定して書籍情報を返すリゾルバを実装します。スキーマでは Query.book(title: String!): Book となっていたやつです。

import { books } from './graphql/data'

const rootResolver: RootResolver = (c) => {
  return {
    book: ({ title }: { title: string }) => {
      return books.find((book) => book.title === title) ?? null;
    },
  };
};

このように書けます。
GraphQL クエリの引数がリゾルバ関数の第一引数に対応します。title という引数名で string 型の値が渡されることを期待するので型を { title: string } とします。

これにて引数を取るフィールドができました。GraphiQL でも応答が返っているのが見えます。

応答が返る。が、作者名は得られない

 

ところが author フィールドが null になっていますね。
それもそのはず、リゾルバが返す値に author フィールドは無いからです。

export const books: Book[] = [
    { id: 0, title: "吾輩は猫である", authorId: 0 },
    { id: 1, title: "こころ", authorId: 0 },
    // ...
];


rootResolver().book({ title: "こころ" });
// => { id: 1, title: "こころ", authorId: 0 }

author フィールドをちゃんと返すために、リゾルバから値を返すときにフィールドを埋めてあげる必要があります。

import { books } from './graphql/data'

const rootResolver: RootResolver = (c) => {
  return {
    book: ({ title }: { title: string }) => {
      const book = books.find((book) => book.title === title);
      if (!book) return null;
      return {
        ...book,
        author: authors.find((author) => author.id === book.authorId) ?? null,
      };
    },
  };
};

...はい、言いたいことは分かります。
API へのリクエストで必ず author が要求されるとは限らないのに、常に返り値に含めておくのは非効率です。型同士が相互に参照していたら無限に大きなオブジェクトを返す必要が出てきます。

 

このような課題に対して GraphQL では Field Resolver で対処するのが普通です。Query に対してのリゾルバではなく (今回の例でいうと) Book 型や Author 型に対するリゾルバを定義してフィールドの値を返せるようにする。

const BookResolvers = {
  author(parent: Book): Author | null {
    if (book.authorId == null) return null;
    return authors.find((author) => author.id === book.authorId) ?? null;
  },
};

こんな感じです。GraphQL で Book.author が必要になったら TypeScript 側で BookResolvers.author(book) が呼ばれてほしい。

 

さて公式のサンプルコードにある RootResolver を使って Field Resolver がどのように実装できるか気になります。調べると、どうやらRootResolver ではダメみたいです。ということで次の節で発展的な例を見ていきましょう。

とりあえずここまでの例を俯瞰するとこんな感じ。

import { type RootResolver, graphqlServer } from "@hono/graphql-server";
import { buildSchema } from "graphql";
import { Hono } from "hono";
import { authors, books } from "./graphql/data.js";

const typeDefs = buildSchema(`
type Author {
  id: Int!
  name: String!
  firstName: String!
  lastName: String!
  books(findTitle: String): [Book!]!
}

type Book {
  id: Int!
  title: String!
  author: Author
}

type Query {
  hello(name: String): String!
  book(title: String!): Book
  books: [Book!]!
}
`);

const rootResolver: RootResolver = (c) => {
  return {
    hello: ({ name = "Hono" }: { name: string }) => `Hello, ${name}!`,

    book: ({ title }: { title: string }) => {
      const book = books.find((book) => book.title === title);
      if (!book) return null;
      return {
        ...book,
        author: authors.find((author) => author.id === book.authorId) ?? null,
      };
    },

    books: () => {
      return books.map((book) => ({
        ...book,
        author: authors.find((author) => author.id === book.authorId) ?? null,
      }));
    },
  };
};

const app = new Hono();

app.get("/", (c) => c.redirect("/graphql"));

app.use(
  "/graphql",
  graphqlServer({
    schema: typeDefs,
    rootResolver,
    graphiql: true,
  }),
);

export default app;

 

さらに発展的な例: Field Resolver

またまた結果だけ先に見たい人のために完成品のコードがここにあります。

github.com

 

公式のサンプルコードでは「リゾルバを定義するには RootResolver を使えよ」という雰囲気を感じるのですが、どうやら RootResolver で定義できるのは Query 型のフィールドに対するリゾルバのみのようです。(だから名前が "Root" Resolver)

では Query 型以外に対するリゾルバはどのように定義できるかというと、RootResolver を辞めればできます。

import { makeExecutableSchema } from '@graphql-tools/schema';
import { buildSchema } from 'graphql';

const typeDefs = buildSchema(`
  type Author {
    id: Int!
    name: String!
    firstName: String!
    lastName: String!
    books(findTitle: String): [Book!]!
  }

  type Book {
    id: Int!
    title: String!
    author: Author
  }

  type Query {
    hello(name: String): String!
    book(title: String!): Book
    books: [Book!]!
  }
`);

const resolvers = {
  Query: {
    book(_parent: unknown, { title }: { title: string }): Book | null {
      return books.find((book) => book.title === title) ?? null;
    },
  },
  Book: {
    author(book: Book): Author | null {
      if (book.authorId == null) return null;
      return authors.find((author) => author.id === book.authorId) ?? null;
    },
  }
};

const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
});

const app = new Hono();

app.use(
  '/graphql',
  graphqlServer({
    schema,
    graphiql: true,
  }),
);

はい、RootResolver を辞めて resolvers という変数にリゾルバ関数を定義しました。
それを const schema = makeExecutableSchema({ typeDefs, resolvers }) で schema としてまとめてから graphqlServer({ schema }) に渡しています。

実際にクエリを叩いてみて、正しく Field Resolver が呼び出されているのが分かります。

書籍から作者情報に正しく辿れる

 

リゾルバ関数の引数が変わっていることにも注目して見ましょう。

  • 第一引数: parent オブジェクト
  • 第二引数: フィールドの引数
  • 第三引数: GraphQL Context

となります。
たとえば Author.books(findTitle: String): [Book!]! のリゾルバの例、

const AuthorResolvers = {
  books(
    parent: Author,
    { findTitle }: { findTitle?: string },
    context: Context,
  ): Book[] {
    console.info({ context});
    const filtered = books.filter((book) => book.authorId === parent.id);
    return findTitle ? filtered.filter((post) => post.title.includes(findTitle)) : filtered;
  },
};

第一引数には親である Author 型のオブジェクトが渡されます。第二引数は GrpahQL クエリの引数ですね。
第三引数は Context です。Hono から import type { Context } from 'hono' で import できる型のオブジェクトが渡されます。

 

私の書いたサンプルでは他にも「schema を別ファイルに切り出す」「リゾルバも別ファイルで定義したものを import する」などの例を見せています。

github.com

 

おまけ: Hono GraphQL Server Middleware の実装を覗いてみる

Hono GraphQL Server Middleware の実装はたったの2ファイルしかなくてとてもシンプルです。よって読み解くのも難しくない。
例えばクエリの実行はこのへん packages/graphql-server/src/index.ts に実装があります。

import { execute } from 'graphql'
import type { FormattedExecutionResult } from 'graphql'

// ...

let result: FormattedExecutionResult
const { rootResolver } = options

try {
  result = await execute({
    schema,
    document: documentAST,
    rootValue: rootResolver ? await rootResolver(c) : null,
    contextValue: c,
    variableValues: variables,
    operationName: operationName,
  })
} catch (contextError: unknown) {
 // ...
}

というわけでクエリ実行に関しては実装者が定義した schema, rootResolver などをほぼそのまま graphql-js の execute() に渡しているだけです。そのため私が今回のサンプルを書くにあたっても Hono に関する知識よりも graphql-js の知識を要する場面が多かったです。

 

まとめ

Hono GraphQL Server Middleware を触ってみました。うーんシンプル。

 

 

私からは以上です。

AIエージェントにコードを書かせてみた感想:AIエージェントが訳分からなくならないために人間が訳分かっておく必要がある

先週 に引き続いて AI エージェントにコードを書かせてみた。 テーマとしてとてもとても典型的で既存のコード例がインターネットに溢れていそうな「テトリス」を選定した。
(選定したというか、職場の id:koudenpa さんが「テトリスでも作らせてみなはれ」と言ったので『いっちょやってみっか』となってやった)

というわけでブラウザで動くテトリスがここにある。(PC にしか対応していないので絶対にスマホでアクセスしないでほしい)

todays-mitsui.github.io

ソースコードはここ。

github.com

 

.cursorrules を辞めて Project Rules を使った

前回は .cursorrules ファイルに AI エージェントへの指示を書いていたが、今後非推奨ということで辞めた。
Project Rules を使うのがいいらしい。

zenn.dev

作りたいもの (今回で云うとテトリス) を小出しで指示するのは面倒なので alwaysApply: true なルールを用意して作りたいものを一通り書いておいた。

github.com

おかげで作りたいものの共通認識が取れた状態で実装を進められた気がする。が、これはテトリスがあまりにも一般的だったからかも知れない。

 

AI エージェントが訳分からなくならないために人間が訳分かってないといけない

AI エージェントと共にプロジェクトを始めると序盤のスピード感がすごい。"とりあえず動くもの" がすぐに出てくる。
そうすると人間サイドとして「なんか知らんけど動いてる!あとはこことここをちょっと変えて...」とノリノリで指示を出しがちである。そのとき目の前のコードをちゃんと読めてない。「なんか正しそうだな」ってくらいの認識で進めてしまう。

そうすると次第に AI エージェントが訳の分からないコードを書き始める。

 

だいたいは私のリクエストに対して既存コードの実装方針に無理があるのが原因。ようはリファクタリングが必要。AI エージェント的には「実装して」と言われたらリファクタリングではなく実装しようとなるので無理してゴリ押し実装することになる。そうすると正しく動かない。
それに対して「修正して」と言ってしまうとリファクタリングではなく修正しようとするので余計に訳が分からなくなる。

そうならないためには人間がちゃんとコードを読んで現状を把握しておく必要がある。

  • いまどんなコードが目の前にあるか
  • そのコードは簡素か、それともいびつか
  • これからやろうとしていることに対して現状の実装は筋がいいか

このあたりは把握して指示出しが必要。人間が訳分かっていれば、AI エージェントが迷走しないように指示出しすることは可能。
問題は序盤の爆速コード生成のなかでどうやって落ち着いてコードを読むか、だろう。個人的には風呂入って一晩寝て起きて ダブルソフト のトースト食べてコーヒー飲んでからコードに向き合うと「次の指示出す前にコード把握するか」と思えがちだ。

 

「既存の挙動を変えないで」と言わないとついでにあれこれやりがち

ある程度コード生成して1ファイルが長くなってきたなと感じたので「コンポーネントを分割して」とか「使ってない変数とかあるのでリファクタして」とか指示出した。
そうしたらなんと!関数の挙動が変わったり CSS が書き換えられてスタイルが変更されていたりする。実に驚くべきことだ。

個人的には「コンポーネント分割」や「リファクタリング」というフレーズには "既存の挙動を変えないまま" というニュアンスを強く感じるのだが AI エージェント的にはその前提は無いっぽい。
「既存の挙動やスタイルを変えてしまわないように注意しながらコンポーネントを分割しよう」と言えば素直に分割だけをやってくれる。やれやれ。

 

ユニットテストや lint は効く

AI エージェントが生成した関数に不具合があることが分かった。そのまま「修正して」だけ言っても時間かかりそうだなと直感したので「修正したいけど、まずはユニットテストを生成して」と指示した。生成されたテストケース を読んだ。ちゃんと的を射ている。 その後、AI エージェント自ら「テストが通るように実装を変更します」と言い出して3回くらいリトライしながら修正していた。

実装しようとしている対象が複雑なとき、まず落ち着いてテストを書くというのは職業プログラマなら当たり前なのだが、AI エージェントにも有効な手法だとは驚いた。

  • 一発でコード生成が難しい程度に複雑なロジックで
  • とはいえテストケースを適切に出せる程度には AI エージェント側が機能を捉えられていて
  • テストケースが相互に干渉しない程度のシンプルさ

そんな問題であれば、テストを書かせることで AI エージェントが訳分からなくなるのをマイルドに回避できる。
ロジックがもう少し複雑になればテストケースを書くのを人間がやったほうが効率的になるのかな、と想像したりする。

 

lint エラーやビルドエラーを見せて修正させるのも有効だった。AI エージェントが自分で問題を見つけるが困難でも。lint コマンドやビルドコマンドを提示して「このコマンドでエラーがなくなるように修正して」と言えば、AI エージェントが繰り返しコマンドを実行してエラーメッセージをヒントに修正をしてくれる。

 

favicon や og:image も生成してくれるが...

<title> を適切に設定して」とお願いしたら「favicon も適切なものを設定したいですね」とか返してきて favicon.svg を作ってくれたので驚いた。
「og:image も作って」と言ったらそれも SVG で作ってくれた。「SVG じゃダメで PNG にする必要があるみたい」と言ったら npx sharp-cli コマンドでうまいことやってくれた。賢い。

Cursor が作ってくれた og:image

さてこの画像が著作権的にセーフなのかというと (ほかの誰かの著作物をほぼそのまま持ってきたりしてないかと言うと)、それはどうなんでしょうね?

まとめ

次はぷよぷよを作ろうかな?

 

 

私からは以上です。