Instruction file imported from Yamamoto1012/MATODO_NEXT (
.cursor/rules/01-functional-programming.mdc). Copyright stays with the author.
globs: .js,.jsx,.ts,.tsx alwaysApply: false
関数型プログラミングの原則
- 純粋関数のみ: 引数のみに依存し、外部状態を変更しない。
- 変数の再代入禁止:
constを用い、新しい値を返して更新を表現する。 - 早期 return: ガード節でネストを浅く保つ。
- 副作用の隔離: 必要な IO は専用フック(例
useFetch,useAnalytics)に分離する。
コード例
1. 純粋関数のみ
引数のみに依存し、外部の状態を変更しない関数を作成します。
// 悪い例: 外部の変数に依存し、それを変更する可能性がある
let minimumAge = 18;
function isAdultBad(age: number): boolean {
// 外部の 'minimumAge' に依存
return age >= minimumAge;
}
function setMinimumAge(newAge: number): void {
// 外部の状態を変更する (副作用)
minimumAge = newAge;
}
// 良い例: 引数として必要な値をすべて受け取り、外部状態に影響されない
function isAdultGood(age: number, legalMinimumAge: number): boolean {
return age >= legalMinimumAge;
}
// より実践的な例: ユーザーの権限チェック
type UserRole = 'admin' | 'editor' | 'viewer';
type Permission = 'read' | 'write' | 'delete';
const rolePermissions: Record<UserRole, Permission[]> = {
admin: ['read', 'write', 'delete'],
editor: ['read', 'write'],
viewer: ['read']
};
function hasPermission(
userRole: UserRole,
requiredPermission: Permission
): boolean {
return rolePermissions[userRole]?.includes(requiredPermission) ?? false;
}
// 使用例
const userRole: UserRole = 'editor';
const canDelete = hasPermission(userRole, 'delete'); // false
const canWrite = hasPermission(userRole, 'write'); // true
2. 変数の再代入禁止
const を使用し、オブジェクトや配列を更新する際は新しいインスタンスを返します。
// 悪い例: let を使用して変数を再代入
let count = 0;
count = count + 1;
// 悪い例: 配列を直接変更 (ミュータブル)
const numbersArrayBad: number[] = [1, 2, 3];
numbersArrayBad.push(4); // 元の配列が変更される
// 悪い例: オブジェクトを直接変更 (ミュータブル)
const userProfileBad = { name: "John", age: 30 };
userProfileBad.age = 31; // 元のオブジェクトが変更される
// 良い例: 新しい値を返すことで更新を表現
const initialCount = 0;
const updatedCount = initialCount + 1;
// 良い例: 配列の更新は新しい配列を返す (イミュータブル)
const numbersArrayGood: readonly number[] = [1, 2, 3];
const newNumbersArray = [...numbersArrayGood, 4];
// numbersArrayGood は [1, 2, 3] のまま
// より実践的な例: ショッピングカートの更新
type CartItem = {
readonly id: string;
readonly name: string;
readonly price: number;
readonly quantity: number;
};
type ShoppingCart = {
readonly items: readonly CartItem[];
readonly total: number;
};
function addItemToCart(
cart: ShoppingCart,
newItem: Omit<CartItem, 'quantity'>,
quantity: number
): ShoppingCart {
const existingItem = cart.items.find(item => item.id === newItem.id);
if (existingItem) {
// 既存アイテムの数量を更新
const updatedItems = cart.items.map(item =>
item.id === newItem.id
? { ...item, quantity: item.quantity + quantity }
: item
);
return {
items: updatedItems,
total: calculateTotal(updatedItems)
};
}
// 新規アイテムを追加
const newCartItem: CartItem = { ...newItem, quantity };
const updatedItems = [...cart.items, newCartItem];
return {
items: updatedItems,
total: calculateTotal(updatedItems)
};
}
function calculateTotal(items: readonly CartItem[]): number {
return items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}
// 使用例
const initialCart: ShoppingCart = {
items: [],
total: 0
};
const newItem = {
id: '1',
name: '商品A',
price: 1000
};
const updatedCart = addItemToCart(initialCart, newItem, 2);
// updatedCart.items = [{ id: '1', name: '商品A', price: 1000, quantity: 2 }]
// updatedCart.total = 2000
3. 早期 return
ガード節を使用してネストを浅く保ち、可読性を高めます。
// 悪い例: 深いネスト
function getDiscountRateBad(userType: string, purchaseAmount: number): number {
let rate = 0;
if (userType === "premium") {
if (purchaseAmount > 10000) {
rate = 0.15;
} else {
rate = 0.1;
}
} else {
if (purchaseAmount > 5000) {
rate = 0.05;
}
}
return rate;
}
// 良い例: 早期 return
function getDiscountRateGood(userType: string, purchaseAmount: number): number {
if (userType !== "premium") {
if (purchaseAmount > 5000) {
return 0.05; // 一般ユーザー、高額購入
}
return 0; // 一般ユーザー、少額購入
}
// 以下、premium ユーザーの場合
if (purchaseAmount > 10000) {
return 0.15; // プレミアムユーザー、高額購入
}
return 0.1; // プレミアムユーザー、少額購入
}
// より実践的な例: フォームバリデーション
type ValidationResult = {
isValid: boolean;
errors: string[];
};
function validateUserInput(
username: string,
email: string,
age: number
): ValidationResult {
const errors: string[] = [];
// 早期 return で無効なケースを先に処理
if (!username) {
return {
isValid: false,
errors: ['ユーザー名は必須です']
};
}
if (username.length < 3) {
errors.push('ユーザー名は3文字以上必要です');
}
if (!email) {
return {
isValid: false,
errors: [...errors, 'メールアドレスは必須です']
};
}
if (!email.includes('@')) {
errors.push('有効なメールアドレスを入力してください');
}
if (age < 0 || age > 150) {
errors.push('有効な年齢を入力してください');
}
return {
isValid: errors.length === 0,
errors
};
}
// 使用例
const result = validateUserInput('', 'invalid-email', 200);
// result = {
// isValid: false,
// errors: ['ユーザー名は必須です']
// }
4. 副作用の隔離
API呼び出しや状態変更のような副作用を伴う処理は、専用の関数やカスタムフック(Reactの場合)に分離します。
// 例: APIからデータを取得する純粋でない関数
// この関数はネットワークリクエストという副作用を持つ
async function fetchUserDataBad(userId: string) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// エラー処理も副作用の一種
console.error("Failed to fetch user data");
return null;
}
return await response.json();
} catch (error) {
console.error("Network error:", error);
return null;
}
}
// 良い例: 副作用を持つ処理をラップする (カスタムフックの概念に近い)
// データ取得ロジックと、その結果を使うロジックを分離
type User = { id: string; name: string };
// 副作用(API通信)を担当する関数
// この関数自体は副作用を持つが、アプリケーションのコアロジックからは分離される
async function fetchUserApi(userId: string): Promise<User | null> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// 実際のエラーハンドリングはより詳細に行うべき
// ここでは簡略化のためコンソール出力
console.error(`API Error: ${response.status}`);
return null;
}
return await response.json() as User;
} catch (error) {
console.error("Network or parsing error:", error);
return null;
}
}
// より実践的な例: React カスタムフックでの副作用の分離
import { useState, useEffect } from 'react';
type UserProfile = {
id: string;
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
};
type UserProfileState = {
data: UserProfile | null;
isLoading: boolean;
error: Error | null;
};
function useUserProfile(userId: string): UserProfileState {
const [state, setState] = useState<UserProfileState>({
data: null,
isLoading: true,
error: null
});
useEffect(() => {
let isMounted = true;
async function fetchProfile() {
try {
const response = await fetch(`/api/users/${userId}/profile`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (isMounted) {
setState({
data,
isLoading: false,
error: null
});
}
} catch (error) {
if (isMounted) {
setState({
data: null,
isLoading: false,
error: error instanceof Error ? error : new Error('Unknown error')
});
}
}
}
fetchProfile();
return () => {
isMounted = false;
};
}, [userId]);
return state;
}
// 使用例
function UserProfileComponent({ userId }: { userId: string }) {
const { data, isLoading, error } = useUserProfile(userId);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!data) return <div>No profile found</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
<p>Theme: {data.preferences.theme}</p>
<p>Notifications: {data.preferences.notifications ? 'On' : 'Off'}</p>
</div>
);
}