今日のゴール
- 型安全APIの利点を理解する
-
defineTupleマクロの使い方をマスターする - 型付きタプルでバグを防ぐ方法を学ぶ
動的型付けの問題
これまでの例では、タプルは動的に型付けされていました。
# 動的型付け: 実行時までエラーがわからない
let t = toTuple(strVal("user"), intVal(25), strVal("alice"))
# 間違ったアクセス(コンパイルは通る)
let age = t[1].strVal # 実行時エラー: intValなのにstrValでアクセス
型安全APIを使うと、このようなエラーをコンパイル時に検出できます。
defineTupleマクロ
defineTuple マクロは、型付きタプルを定義します。
import ninda/macros/typed
# 型付きタプルの定義
defineTuple(User):
name: string
age: int64
# 使用例
let user = User(name: "alice", age: 25)
echo user.name # "alice" (string型として保証)
echo user.age # 25 (int64型として保証)
生成されるコード
defineTuple(User) は以下を自動生成します:
| 生成物 | 説明 |
|---|---|
type User |
オブジェクト型 |
toTuple(User) |
User → Tuple変換 |
fromTuple(User, Tuple) |
Tuple → User変換 |
UserPattern(...) |
型安全なパターンビルダー |
型付きタプルの使用
defineTuple(Task):
taskType: string
taskId: int64
priority: int64
proc example() {.async.} =
let ts = newSpaceManager().space()
# 型付きオブジェクトを作成
let task = Task(taskType: "process", taskId: 1001, priority: 5)
# Tupleに変換して書き込み
await ts.writeAsync(task.toTuple())
# パターンで読み取り
let result = await ts.readAsync(TaskPattern(taskType = some("process")))
# 型付きオブジェクトに変換
let readTask = Task.fromTuple(result)
# 型安全なアクセス
echo readTask.taskId # int64として保証
echo readTask.priority # int64として保証
ポイント:
- フィールド名でアクセス(インデックスではなく)
- IDEの補完が効く
- 型の不一致はコンパイルエラー
パターンビルダーの活用
*Pattern 関数で柔軟なパターンを作成:
defineTuple(Order):
orderId: string
customerId: int64
status: string
amount: float64
# 全オーダー(全フィールドワイルドカード)
let allPattern = OrderPattern()
# 特定顧客のオーダー
let customer100Pattern = OrderPattern(customerId = some(100'i64))
# pending状態のオーダー
let pendingPattern = OrderPattern(status = some("pending"))
# 複数条件の組み合わせ
let specificPattern = OrderPattern(
customerId = some(100'i64),
status = some("pending")
)
ポイント:
-
some(値)で具体値を指定 - 指定しないフィールドはワイルドカード(任意の値にマッチ)
複数の型付きタプル
アプリケーションでは複数の型を定義します。
# ドメインモデルの定義
defineTuple(Product):
sku: string
name: string
price: float64
stock: int64
defineTuple(CartItem):
cartId: string
productSku: string
quantity: int64
defineTuple(OrderEvent):
eventType: string
orderId: string
timestamp: int64
各型は独立したスペースで管理するのがベストプラクティス:
let products = manager.space("products")
let carts = manager.space("carts")
let events = manager.space("events")
型安全APIの利点
| 観点 | 動的型付け | 型安全API |
|---|---|---|
| エラー検出 | 実行時 | コンパイル時 |
| IDEサポート | 限定的 | 完全(補完、型ヒント) |
| リファクタリング | 危険 | 安全 |
| ドキュメント | コメント頼り | 型が仕様 |
ベストプラクティス
1. 型定義をモジュールにまとめる
# types.nim
defineTuple(User):
userId: string
name: string
role: string
defineTuple(Session):
sessionId: string
userId: string
expiresAt: int64
2. パターンを定数化
const
ALL_USERS = UserPattern()
ADMIN_USERS = UserPattern(role = some("admin"))
3. ラッパー関数を提供
proc writeUser*(ts: TupleSpace, user: User) {.async.} =
await ts.writeAsync(user.toTuple())
proc readUser*(ts: TupleSpace, userId: string): Future[Option[User]] {.async.} =
let result = await ts.tryReadAsync(UserPattern(userId = some(userId)))
if result.isSome:
return some(User.fromTuple(result.get()))
return none(User)
まとめ
学んだこと
| トピック | ポイント |
|---|---|
| defineTuple | 型付きタプルを定義するマクロ |
| toTuple/fromTuple | 型変換 |
| パターンビルダー | 型安全なパターン作成 |
| 利点 | コンパイル時エラー検出、IDE支援 |
いつ使うか
| 場面 | 推奨 |
|---|---|
| 本番コード | 型安全API |
| プロトタイプ | 動的型付け(手軽さ優先) |
| 外部データ連携 | 型安全API + バリデーション |
演習問題
問題12-1: Eコマースモデル
以下のドメインモデルを型安全APIで定義してください:
- Customer(customerId, name, email)
- Product(productId, name, price, category)
- Order(orderId, customerId, productId, quantity)
ヒント
- 各モデルを
defineTupleで定義 - フィールドの型: string(ID, name, email, category)、float64(price)、int64(quantity)
- 各モデル用のスペースを作成: customers, products, orders
問題12-2: パターン検索
問題12-1のモデルを使って、以下の検索を実装してください:
- 特定カテゴリの商品一覧
- 特定顧客の注文履歴
- 特定商品の注文一覧
ヒント
-
ProductPattern(category = some("electronics"))のようにパターンを作成 -
readAllAsyncで複数件取得 - 結果は
seq[Tuple]なので、Product.fromTupleで変換
問題12-3: 安全な型変換
外部から受け取ったJSONを型安全に変換する関数を実装してください。変換に失敗した場合はOption[T]でnoneを返すようにしてください。
ヒント
-
try-exceptでエラーをキャッチ - 要素数が違う、型が違うなどのケースに対応
-
safeParse[T](tup: Tuple): Option[T]のようなジェネリック関数を作成 - 失敗時は
none(T)、成功時はsome(T.fromTuple(tup))