Functional Programming Fundamentals について
関数型プログラミングを学ぶ開発者向けのスキルです。TypeScriptの例を使い、純粋関数、カリー化、合成、ポイントフリースタイル、一級関数を fp-ts の型に進む前に解説します。
主な機能
- 01純粋関数と参照透過性
- 02カリー化と部分適用
- 03関数合成とパイプライン
- 04ポイントフリースタイル
- 05一級関数と高階関数
活用シーン
- 01関数型プログラミングの基礎理解
- 02TypeScriptで合成可能な関数を作成
- 03fp-tsの型を学ぶ前の準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ whatiskadudoing/ fp-ts-skills --skill functional-programming-fundamentalsインストール後、Claude Code で/functional-programming-fundamentalsを入力して読み込まれたか確認します。
詳細
関数型プログラミングの基礎
このスキルでは、関数型プログラミングの基礎概念を扱います。Option、Either、Task などの fp-ts の型に進む前に、これらを習得しましょう。これらの概念は、関数型プログラミングの言語やライブラリ全般に当てはまります。
なぜ関数型プログラミングなのか?
関数型プログラミングには、次のような利点があります。
- 予測可能性:純粋関数は、同じ入力に対して常に同じ出力を生成します
- テストのしやすさ:グローバル状態や依存関係をモックする必要がありません
- 合成可能性:小さな関数を組み合わせて複雑な振る舞いを実現できます
- 保守性:分離された要素は理解しやすく、変更も容易です
- 並列化:純粋関数は安全に並行実行できます
1. 純粋関数と参照透過性
純粋関数とは?
純粋関数には、次の2つの性質があります。
- 同じ入力には同じ出力:同じ引数を与えると、常に同じ結果を返します
- 副作用がない:外部状態を変更したり、I/O を実行したり、観測可能な変化を引き起こしたりしません
なぜ重要なのか
純粋関数には、次のような利点があります。
- キャッシュ可能:決定的なため、結果をメモ化できます
- 移植可能:外部状態への隠れた依存関係がありません
- テストのしやすさ:グローバル状態のセットアップや後片付けが不要です
- 並列実行可能:競合状態を起こさずに安全に並行実行できます
- 局所的に推論可能:システム全体を知らなくても理解できます
例
// PURE: Same input always gives same output
const add = (x: number, y: number): number => x + y
const greet = (name: string): string => `Hello, ${name}!`
const head = <T>(arr: readonly T[]): T | undefined => arr[0]
// IMPURE: Depends on external state
let counter = 0
const incrementCounter = (): number => {
counter += 1 // Mutates external state
return counter
}
// IMPURE: Different output for same input
const randomNumber = (): number => Math.random()
// IMPURE: Performs I/O
const logMessage = (msg: string): void => {
console.log(msg) // Side effect: writes to console
}
// IMPURE: Depends on mutable external state
const config = { apiUrl: 'https://api.example.com' }
const getApiUrl = (): string => config.apiUrl // Could return different values
参照透過性
式の値に置き換えてもプログラムの振る舞いが変わらない場合、その式は参照透過性を持ちます。
// Referentially transparent
const double = (x: number): number => x * 2
const result = double(5) + double(5)
// Can be replaced with: 10 + 10 = 20
// NOT referentially transparent
let count = 0
const incrementAndGet = (): number => {
count += 1
return count
}
const result2 = incrementAndGet() + incrementAndGet()
// Cannot replace with values: first call returns 1, second returns 2
// Result is 3, not 1 + 1 = 2
不純関数を純粋関数にする
// IMPURE: Modifies input
const addItem = (arr: string[], item: string): void => {
arr.push(item) // Mutates the original array
}
// PURE: Returns new array
const addItem = (arr: readonly string[], item: string): readonly string[] =>
[...arr, item]
// IMPURE: Depends on Date
const getAge = (birthYear: number): number =>
new Date().getFullYear() - birthYear
// PURE: Accept date as parameter (dependency injection)
const getAge = (birthYear: number, currentYear: number): number =>
currentYear - birthYear
// IMPURE: Reads from database
const getUser = async (id: string): Promise<User> =>
await database.find(id)
// Functional approach: Return a description of the effect
// (This is the concept behind Task and IO in fp-ts)
const getUser = (id: string): Task<User> =>
() => database.find(id)
よくある間違い
// MISTAKE 1: Mutating arguments
const sortArray = (arr: number[]): number[] => {
return arr.sort((a, b) => a - b) // sort() mutates the original!
}
// FIX: Create a copy first
const sortArray = (arr: readonly number[]): number[] =>
[...arr].sort((a, b) => a - b)
// MISTAKE 2: Using Date, Math.random, or other non-deterministic APIs
const generateId = (): string =>
`id-${Date.now()}-${Math.random()}`
// FIX: Accept dependencies as parameters
const generateId = (timestamp: number, random: number): string =>
`id-${timestamp}-${random}`
// MISTAKE 3: Hidden dependencies via closure
const multiplier = 2
const double = (x: number): number => x * multiplier // Captures external variable
// FIX: Make dependencies explicit
const multiply = (factor: number) => (x: number): number => x * factor
const double = multiply(2)
2. カリー化と部分適用
カリー化とは?
カリー化とは、複数の引数を取る関数を、それぞれ1つの引数を取る関数の連鎖に変換することです。
// Uncurried: takes all arguments at once
const add = (x: number, y: number, z: number): number => x + y + z
add(1, 2, 3) // 6
// Curried: takes one argument at a time
const addCurried = (x: number) => (y: number) => (z: number): number => x + y + z
addCurried(1)(2)(3) // 6
なぜ重要なのか
カリー化には、次のような利点があります。
- 部分適用:一部の引数を渡して、特化した関数を作成できます
- 合成:単一引数の関数は簡潔に合成できます
- 再利用性:1つの定義から、関連する関数の集合を作成できます
- 計算の遅延:引数を利用可能になった時点で渡せます
実際のカリー化
// Curried function definition
const multiply = (x: number) => (y: number): number => x * y
// Create specialized functions through partial application
const double = multiply(2)
const triple = multiply(3)
double(5) // 10
triple(5) // 15
// Curried comparison helpers
const greaterThan = (threshold: number) => (value: number): boolean =>
value > threshold
const isAdult = greaterThan(17)
const isExpensive = greaterThan(100)
isAdult(25) // true
isExpensive(50) // false
配列メソッドのカリー化
// Curried filter predicate
const filterBy = <T>(predicate: (item: T) => boolean) =>
(items: readonly T[]): T[] => items.filter(predicate)
// Create reusable filters
const keepPositive = filterBy((n: number) => n > 0)
const keepEven = filterBy((n: number) => n % 2 === 0)
keepPositive([-1, 2, -3, 4]) // [2, 4]
keepEven([1, 2, 3, 4, 5, 6]) // [2, 4, 6]
// Curried map transformer
const mapWith = <A, B>(fn: (a: A) => B) =>
(items: readonly A[]): B[] => items.map(fn)
const doubleAll = mapWith((n: number) => n * 2)
const stringify = mapWith(String)
doubleAll([1, 2, 3]) // [2, 4, 6]
stringify([1, 2, 3]) // ['1', '2', '3']
部分適用とカリー化の違い
// Partial application: fixing some arguments of a function
const greet = (greeting: string, name: string): string =>
`${greeting}, ${name}!`
// Using bind for partial application (uncurried)
const sayHello = greet.bind(null, 'Hello')
sayHello('Alice') // "Hello, Alice!"
// Curried version is naturally partially applicable
const greetCurried = (greeting: string) => (name: string): string =>
`${greeting}, ${name}!`
const sayHello = greetCurried('Hello')
const sayGoodbye = greetCurried('Goodbye')
sayHello('Alice') // "Hello, Alice!"
sayGoodbye('Bob') // "Goodbye, Bob!"
引数の順序が重要
// BAD: Data first prevents easy partial application
const map = <A, B>(arr: A[], fn: (a: A) => B): B[] => arr.map(fn)
// Must use anonymous function to specialize
const doubleAll = (arr: number[]) => map(arr, n => n * 2)
// GOOD: Data last enables clean partial application
const map = <A, B>(fn: (a: A) => B) => (arr: readonly A[]): B[] => arr.map(fn)
// Specialize by supplying the function
const doubleAll = map((n: number) => n * 2)
const stringify = map(String)
// This is why fp-ts uses data-last curried functions
import * as A from 'fp-ts/Array'
import { pipe } from 'fp-ts/function'
pipe(
[1, 2, 3],
A.map(n => n * 2), // A.map takes fn first, returns function expecting array
A.filter(n => n > 2) // Same pattern
)
よくある間違い
// MISTAKE 1: Inconsistent argument order
const process = (config: Config) => (data: Data) => (options: Options) => ...
const handle = (data: Data) => (config: Config) => ... // Inconsistent!
// FIX: Establish convention - typically: config -> options -> data
const process = (config: Config) => (options: Options) => (data: Data) => ...
const handle = (config: Config) => (data: Data) => ...
// MISTAKE 2: Over-currying simple functions
const add = (a: number) => (b: number) => (c: number) => (d: number) =>
a + b + c + d
add(1)(2)(3)(4) // Tedious to call
// FIX: Curry only to the level needed for your use case
const add = (a: number, b: number, c: number, d: number) => a + b + c + d
// Or group related parameters
const addPairs = (a: number, b: number) => (c: number, d: number) =>
a + b + c + d
// MISTAKE 3: Forgetting to curry when using with pipe
const formatPrice = (currency: string, amount: number): string =>
`${currency}${amount.toFixed(2)}`
pipe(
100,
formatPrice('$', ???) // Can't use in pipe - needs both args!
)
// FIX: Curry with data last
const formatPrice = (currency: string) => (amount: number): string =>
`${currency}${amount.toFixed(2)}`
pipe(
100,
formatPrice('$') // Works!
) // "$100.00"
3. 関数合成
関数合成とは?
関数合成とは、2つ以上の関数を組み合わせて新しい関数を作成することです。ある関数の出力が次の関数の入力になります。
// Mathematical composition: (f . g)(x) = f(g(x))
const compose = <A, B, C>(f: (b: B) => C, g: (a: A) => B) =>
(x: A): C => f(g(x))
const addOne = (x: number): number => x + 1
const double = (x: number): number => x * 2
const addOneThenDouble = compose(double, addOne)
addOneThenDouble(5) // double(addOne(5)) = double(6) = 12
なぜ重要なのか
合成には、次のような利点があります。
- 単純な要素から複雑な振る舞いを構築:小さく目的を絞った関数を組み合わせて、強力な変換を実現できます
- 再利用性:合成した関数をさらに合成できます
- 読みやすさ:各ステップの目的が明確です
- テスト:小さな要素を個別にテストでき、合成の正しさは数学的に保証されています
Pipe と Flow(左から右への合成)
従来の数学的な合成は右から左へ読み進めるため、分かりにくい場合があります。pipe と flow を使うと、左から右へ合成できます。
import { pipe, flow } from 'fp-ts/function'
const addOne = (x: number): number => x + 1
const double = (x: number): number => x * 2
const toString = (x: number): string => `Value: ${x}`
// pipe: Execute immediately with a value
const result = pipe(
5,
addOne, // 6
double, // 12
toString // "Value: 12"
)
// flow: Create a reusable function
const transform = flow(
addOne,
double,
toString
)
transform(5) // "Value: 12"
transform(10) // "Value: 22"
複数の型を使った合成
// Functions don't need matching types, just compatible connections
const parseNumber = (s: string): number => parseInt(s, 10)
const isEven = (n: number): boolean => n % 2 === 0
const toYesNo = (b: boolean): string => b ? 'Yes' : 'No'
const isInputEven = flow(
parseNumber, // string -> number
isEven, // number -> boolean
toYesNo // boolean -> string
)
isInputEven('4') // "Yes"
isInputEven('7') // "No"
パイプラインの構築
import { pipe, flow } from 'fp-ts/function'
interface User {
name: string
email: string
age: number
}
// Small, focused functions
const getName = (user: User): string => user.name
const toUpperCase = (s: string): string => s.toUpperCase()
const addGreeting = (name: string): string => `Hello, ${name}!`
const addExcitement = (s: string): string => `${s}!!!`
// Compose into a pipeline
const greetUser = flow(
getName,
toUpperCase,
addGreeting,
addExcitement
)
greetUser({ name: 'alice', email: '[email protected]', age: 30 })
// "Hello, ALICE!!!!"
// Or use pipe for one-off transformations
const greeting = pipe(
{ name: 'bob', email: '[email protected]', age: 25 },
getName,
toUpperCase,
addGreeting
)
// "Hello, BOB!"
合成の結合律
合成には結合律があります。(f . g) . h = f . (g . h)
const f = (x: number): number => x + 1
const g = (x: number): number => x * 2
const h = (x: number): number => x - 3
// These are equivalent:
const way1 = flow(flow(h, g), f) // (f . g) . h
const way2 = flow(h, flow(g, f)) // f . (g . h)
const way3 = flow(h, g, f) // Direct composition
way1(10) // 15
way2(10) // 15
way3(10) // 15
よくある間違い
// MISTAKE 1: Composing functions with incompatible types
const toString = (n: number): string => String(n)
const double = (n: number): number => n * 2
const bad = flow(toString, double) // Type error! double expects number, gets string
// MISTAKE 2: Trying to compose multi-argument functions
const add = (a: number, b: number): number => a + b
const double = (n: number): number => n * 2
const bad = flow(add, double) // Won't work - add takes 2 args
// FIX: Curry first
const add = (a: number) => (b: number): number => a + b
const addFiveThenDouble = flow(add(5), double)
addFiveThenDouble(3) // 16
// MISTAKE 3: Long unreadable pipelines
const result = pipe(
data,
fn1, fn2, fn3, fn4, fn5, fn6, fn7, fn8, fn9, fn10 // What does this do?
)
// FIX: Group related transformations and name them
const parseInput = flow(fn1, fn2, fn3)
const validateData = flow(fn4, fn5)
const formatOutput = flow(fn6, fn7, fn8, fn9, fn10)
const process = flow(parseInput, validateData, formatOutput)
4. ポイントフリースタイル
ポイントフリースタイルとは?
ポイントフリー(または暗黙的)プログラミングでは、引数を明示せずに関数を定義します。「ポイント」とは引数のことです。
// Pointed: explicitly names the argument
const double = (x: number): number => x * 2
// Pointfree: no explicit argument
const double = multiply(2) // Assuming multiply is curried
なぜ重要なのか
ポイントフリースタイルには、次のような利点があります。
- ノイズを減らす:データの受け渡しではなく、変換に集中できます
- 合成を促す:
flow/pipeと自然に組み合わせられます - パターンを明らかにする:計算の構造が見えやすくなります
- 間違いを防ぐ:誤字やシャドーイングの原因になる変数名がありません
実際のポイントフリースタイル
import { pipe, flow } from 'fp-ts/function'
import * as A from 'fp-ts/Array'
// Pointed style
const getActiveUserNames = (users: User[]): string[] =>
users
.filter(user => user.isActive)
.map(user => user.name)
// Pointfree style
const isActive = (user: User): boolean => user.isActive
const getName = (user: User): string => user.name
const getActiveUserNames = flow(
A.filter(isActive),
A.map(getName)
)
// Both work the same:
getActiveUserNames(users)
ポイントフリー関数の構築
// Utility functions enable pointfree style
const prop = <T, K extends keyof T>(key: K) => (obj: T): T[K] => obj[key]
const equals = <T>(target: T) => (value: T): boolean => value === target
const gt = (threshold: number) => (value: number): boolean => value > threshold
interface Product {
name: string
price: number
category: string
}
// Pointed
const getExpensiveElectronics = (products: Product[]): Product[] =>
products.filter(p => p.category === 'electronics' && p.price > 100)
// Pointfree with helpers
const isElectronics = flow(prop<Product, 'category'>('category'), equals('electronics'))
const isExpensive = flow(prop<Product, 'price'>('price'), gt(100))
const both = <T>(f: (t: T) => boolean, g: (t: T) => boolean) =>
(t: T): boolean => f(t) && g(t)
const getExpensiveElectronics = A.filter(both(isElectronics, isExpensive))
ポイントフリーが役立つ場面
// GOOD: Simple transformations
const doubleAll = A.map((n: number) => n * 2) // Semi-pointfree
const keepPositive = A.filter((n: number) => n > 0)
// BETTER: With named predicates
const double = (n: number): number => n * 2
const isPositive = (n: number): boolean => n > 0
const doubleAll = A.map(double) // Fully pointfree
const keepPositive = A.filter(isPositive)
// Compose them
const processNumbers = flow(
keepPositive,
doubleAll
)
Pointfree を避けるべき場合
// AVOID: When it obscures meaning
const mystery = flow(
A.map(flow(prop('x'), add(1))),
A.filter(flow(prop('y'), gt(5))),
A.reduce(0, flow(([acc, item]) => acc + item.z))
)
// PREFER: Clear variable names when logic is complex
const incrementX = (item: Item): Item => ({ ...item, x: item.x + 1 })
const hasLargeY = (item: Item): boolean => item.y > 5
const sumZ = (items: Item[]): number =>
items.reduce((acc, item) => acc + item.z, 0)
const process = flow(
A.map(incrementX),
A.filter(hasLargeY),
sumZ
)
// AVOID: Pointfree gymnastics
const weird = flip(compose(flip(map), filter))(isEven)(double)
// PREFER: Readable code
const doubleEvens = flow(
A.filter(isEven),
A.map(double)
)
よくある間違い
// MISTAKE 1: Forcing pointfree when it's unclear
const process = flow(
A.map(flow(
juxt([prop('a'), prop('b')]),
apply(add)
))
)
// FIX: Use pointed style when clearer
const process = A.map(({ a, b }) => a + b)
// MISTAKE 2: Missing type annotations in pointfree
const double = A.map(n => n * 2) // n is 'unknown' without context
// FIX: Add type annotation
const double = A.map((n: number) => n * 2)
// MISTAKE 3: Mixing pointed and pointfree awkwardly
const process = (arr: number[]) => flow(
A.filter(n => n > 0),
A.map(n => n * 2)
)(arr)
// FIX: Choose one style
// Pointfree:
const process = flow(
A.filter((n: number) => n > 0),
A.map(n => n * 2)
)
// Or pointed:
const process = (arr: number[]): number[] =>
pipe(arr, A.filter(n => n > 0), A.map(n => n * 2))
5. 第一級関数
第一級関数とは?
JavaScript/TypeScript では、関数は第一級の要素です。関数は次のように扱えます。
- 変数に代入する
- 引数として渡す
- 別の関数から返す
- データ構造に格納する
重要な理由
第一級関数を使うと、次のことが可能になります。
- 高階関数:関数を受け取る、または返す関数
- コールバック:振る舞いをデータとして渡す
- クロージャ:自身の環境を取り込む関数
- 関数合成:関数を組み合わせてプログラムを構築すること
ラッパー関数を避ける
よくあるアンチパターンは、必要のない関数のラップです。
// BAD: Unnecessary wrapper function
const numbers = [1, 2, 3, 4, 5]
// Don't do this:
numbers.map(x => double(x))
numbers.filter(x => isPositive(x))
numbers.forEach(x => console.log(x))
// GOOD: Pass the function directly
numbers.map(double)
numbers.filter(isPositive)
numbers.forEach(console.log)
// The wrapper adds nothing but noise
ラッパーが必要な場合
// NEEDED: When you need to modify arguments
const users = [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]
// getName expects a User, but we want just the index
users.map((user, index) => `${index}: ${user.name}`) // Wrapper needed
// NEEDED: When you need to ignore arguments
const fetchAll = (urls: string[]) =>
urls.map(url => fetch(url)) // fetch has many optional params
// If we did urls.map(fetch), TypeScript might complain about
// the (value, index, array) signature of map's callback
// NEEDED: When changing arity
const add = (a: number, b: number): number => a + b
[1, 2, 3].map(n => add(n, 10)) // Must wrap since map passes 3 args
// BETTER: Curry the function
const add = (b: number) => (a: number): number => a + b
[1, 2, 3].map(add(10)) // [11, 12, 13]
高階関数
// Function that returns a function
const multiply = (x: number) => (y: number): number => x * y
const double = multiply(2)
const triple = multiply(3)
// Function that takes a function
const applyTwice = <T>(fn: (t: T) => T) => (value: T): T =>
fn(fn(value))
const addOne = (n: number): number => n + 1
const addTwo = applyTwice(addOne)
addTwo(5) // 7
// Function that does both
const compose = <A, B, C>(f: (b: B) => C) => (g: (a: A) => B) => (x: A): C =>
f(g(x))
const increment = (n: number): number => n + 1
const toString = (n: number): string => String(n)
const incrementThenStringify = compose(toString)(increment)
incrementThenStringify(5) // "6"
メソッド参照
// Be careful with method references and 'this'
// PROBLEM: Losing 'this' context
const obj = {
value: 42,
getValue() {
return this.value
}
}
const getValue = obj.getValue
getValue() // undefined or error - 'this' is lost
// SOLUTION 1: Bind the method
const getValue = obj.getValue.bind(obj)
getValue() // 42
// SOLUTION 2: Arrow function wrapper
const getValue = () => obj.getValue()
getValue() // 42
// SOLUTION 3: Use static functions (FP approach)
const getValue = (obj: { value: number }): number => obj.value
getValue({ value: 42 }) // 42
よくある間違い
// MISTAKE 1: Unnecessary wrappers
const names = users.map(user => getName(user))
// FIX:
const names = users.map(getName)
// MISTAKE 2: Wrapping to match types incorrectly
const numbers = ['1', '2', '3'].map(s => parseInt(s)) // Seems fine...
// Actually problematic: parseInt takes (string, radix)
// map passes (value, index, array)
// parseInt('2', 1) is NaN!
// FIX: Explicit wrapper when semantics differ
const numbers = ['1', '2', '3'].map(s => parseInt(s, 10))
// Or use Number:
const numbers = ['1', '2', '3'].map(Number)
// MISTAKE 3: Not leveraging first-class functions
const process = (items: Item[], transform: (item: Item) => Item) => {
const result = []
for (const item of items) {
result.push(transform(item))
}
return result
}
// FIX: Just use map
const process = (items: Item[], transform: (item: Item) => Item) =>
items.map(transform)
// Or even simpler - don't wrap map at all:
items.map(transform)
// MISTAKE 4: Immediately invoking returned functions
const createAdder = (x: number) => (y: number) => x + y
// Wrong - this calls the inner function immediately with undefined
const add5 = createAdder(5)() // NaN
// Right - store the function for later use
const add5 = createAdder(5)
add5(3) // 8
実践演習
演習 1:純粋関数
これらの関数が不純である理由を特定し、純粋関数に書き換えてください。
// 1a. Makes HTTP call
const fetchUser = async (id: string) => {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
// 1b. Uses current time
const isExpired = (expiryDate: Date) => {
return expiryDate < new Date()
}
// 1c. Mutates input
const addToCart = (cart: CartItem[], item: CartItem) => {
cart.push(item)
return cart
}
// 1d. Depends on global config
const config = { taxRate: 0.08 }
const calculateTotal = (subtotal: number) => {
return subtotal * (1 + config.taxRate)
}
解答
// 1a. Return a description of the effect (Task pattern)
type FetchUser = (id: string) => () => Promise<User>
const fetchUser: FetchUser = (id) => () =>
fetch(`/api/users/${id}`).then(r => r.json())
// 1b. Accept current time as parameter
const isExpired = (expiryDate: Date, now: Date): boolean =>
expiryDate < now
// 1c. Return new array instead of mutating
const addToCart = (cart: readonly CartItem[], item: CartItem): CartItem[] =>
[...cart, item]
// 1d. Accept config as parameter
const calculateTotal = (taxRate: number) => (subtotal: number): number =>
subtotal * (1 + taxRate)
// Or group them
interface TaxConfig { taxRate: number }
const calculateTotal = (config: TaxConfig, subtotal: number): number =>
subtotal * (1 + config.taxRate)
演習 2:カリー化と部分適用
これらの関数をカリー化し、特化したバージョンを作成してください。
// 2a. Convert to curried form
const formatDate = (locale: string, options: Intl.DateTimeFormatOptions, date: Date): string =>
date.toLocaleDateString(locale, options)
// Create: formatUSDate, formatShortDate
// 2b. Convert to curried form
const clamp = (min: number, max: number, value: number): number =>
Math.max(min, Math.min(max, value))
// Create: clampPercentage (0-100), clampByte (0-255)
// 2c. Convert to curried form with good argument order
const hasProperty = (obj: object, key: string): boolean =>
key in obj
// Should work with: users.filter(hasProperty(???))
解答
// 2a.
const formatDate = (locale: string) =>
(options: Intl.DateTimeFormatOptions) =>
(date: Date): string =>
date.toLocaleDateString(locale, options)
const formatUSDate = formatDate('en-US')
const formatShortDate = formatUSDate({ month: 'short', day: 'numeric' })
formatShortDate(new Date()) // "Jan 29"
// 2b.
const clamp = (min: number) => (max: number) => (value: number): number =>
Math.max(min, Math.min(max, value))
const clampPercentage = clamp(0)(100)
const clampByte = clamp(0)(255)
clampPercentage(150) // 100
clampByte(-10) // 0
// 2c. Reorder arguments: key first (config), object last (data)
const hasProperty = (key: string) => (obj: object): boolean =>
key in obj
const hasEmail = hasProperty('email')
const hasId = hasProperty('id')
users.filter(hasEmail) // Users with email property
演習 3:関数合成
flow と pipe を使って、これらのパイプラインを構築してください。
import { pipe, flow } from 'fp-ts/function'
import * as A from 'fp-ts/Array'
interface Product {
name: string
price: number
category: string
inStock: boolean
}
// 3a. Create a pipeline that:
// - Filters to products in stock
// - Filters to products under $50
// - Maps to product names
// - Sorts alphabetically
// 3b. Create a reusable pipeline that:
// - Takes a string
// - Trims whitespace
// - Converts to lowercase
// - Replaces spaces with hyphens
// - Prefixes with "slug-"
// 3c. Create a number processing pipeline that:
// - Filters out negative numbers
// - Doubles each number
// - Sums all numbers
// - Returns "Total: X"
解答
// 3a.
const getAffordableInStockNames = (products: Product[]): string[] =>
pipe(
products,
A.filter(p => p.inStock),
A.filter(p => p.price < 50),
A.map(p => p.name),
A.sort((a, b) => a.localeCompare(b))
)
// Or as a reusable function:
const getAffordableInStockNames = flow(
A.filter((p: Product) => p.inStock),
A.filter(p => p.price < 50),
A.map(p => p.name),
A.sort<string>((a, b) => a.localeCompare(b))
)
// 3b.
const slugify = flow(
(s: string) => s.trim(),
s => s.toLowerCase(),
s => s.replace(/\s+/g, '-'),
s => `slug-${s}`
)
slugify(' Hello World ') // "slug-hello-world"
// 3c.
const isPositive = (n: number): boolean => n >= 0
const double = (n: number): number => n * 2
const sum = (numbers: number[]): number =>
numbers.reduce((acc, n) => acc + n, 0)
const formatTotal = (n: number): string => `Total: ${n}`
const processNumbers = flow(
A.filter(isPositive),
A.map(double),
sum,
formatTotal
)
processNumbers([-1, 2, 3, -4, 5]) // "Total: 20"
演習 4:Pointfree スタイル
適切な場合は、これらの関数を Pointfree スタイルにリファクタリングしてください。
// 4a. Refactor to pointfree
const getAdultNames = (users: User[]): string[] =>
users
.filter(user => user.age >= 18)
.map(user => user.name)
// 4b. Refactor to pointfree
const sumPrices = (products: Product[]): number =>
products.reduce((total, product) => total + product.price, 0)
// 4c. Decide: Should this be pointfree or not? Why?
const formatUserForDisplay = (user: User): string =>
`${user.name} (${user.email}) - ${user.isActive ? 'Active' : 'Inactive'}`
解答
// 4a. Good candidate for pointfree
const isAdult = (user: User): boolean => user.age >= 18
const getName = (user: User): string => user.name
const getAdultNames = flow(
A.filter(isAdult),
A.map(getName)
)
// 4b. Partially pointfree - the reducer is clearer pointed
const prop = <T, K extends keyof T>(key: K) => (obj: T): T[K] => obj[key]
const sum = (numbers: number[]): number =>
numbers.reduce((a, b) => a + b, 0)
const sumPrices = flow(
A.map(prop<Product, 'price'>('price')),
sum
)
// Or keep it simple:
const getPrice = (p: Product): number => p.price
const sumPrices = flow(A.map(getPrice), sum)
// 4c. Keep pointed - the string template is clearer with explicit access
const formatUserForDisplay = (user: User): string =>
`${user.name} (${user.email}) - ${user.isActive ? 'Active' : 'Inactive'}`
// Pointfree version would be overly complex:
const formatUserForDisplay = flow(
user => [user.name, user.email, user.isActive] as const,
([name, email, isActive]) =>
`${name} (${email}) - ${isActive ? 'Active' : 'Inactive'}`
)
// This is worse - no benefit, harder to read
演習 5:第一級関数
これらの不要なラッパーを取り除き、コードを改善してください。
// 5a. Remove unnecessary wrappers
const results = items
.map(item => processItem(item))
.filter(result => isValid(result))
.forEach(result => logResult(result))
// 5b. Fix the parseInt issue
const numbers = ['1', '2', '3', '10', '11'].map(parseInt)
// Current result: [1, NaN, NaN, 3, 4] - Why? Fix it.
// 5c. Create a safe version of this
const handlers = {
onClick: (e: Event) => handleClick(e),
onSubmit: (e: Event) => handleSubmit(e),
onHover: (e: Event) => handleHover(e),
}
// Too repetitive - can we do better?
解答
// 5a. Pass functions directly
const results = items
.map(processItem)
.filter(isValid)
.forEach(logResult)
// 5b. parseInt receives (value, index, array) from map
// parseInt('2', 1) interprets '2' as base-1 (invalid) = NaN
// parseInt('3', 2) interprets '3' as base-2 (invalid) = NaN
// parseInt('10', 3) interprets '10' as base-3 = 3
// parseInt('11', 4) interprets '11' as base-4 = 5
// Fix: Use wrapper with explicit radix
const numbers = ['1', '2', '3', '10', '11'].map(s => parseInt(s, 10))
// Or use Number (doesn't have radix parameter)
const numbers = ['1', '2', '3', '10', '11'].map(Number)
// Result: [1, 2, 3, 10, 11]
// 5c. If handlers match the expected signature, just use them directly
const handlers = {
onClick: handleClick,
onSubmit: handleSubmit,
onHover: handleHover,
}
// Or if you need to create them dynamically:
const createHandlers = <T extends Record<string, (e: Event) => void>>(
handlerMap: T
): T => handlerMap
const handlers = createHandlers({
onClick: handleClick,
onSubmit: handleSubmit,
onHover: handleHover,
})
まとめ
| 概念 | 要点 | 利点 |
|---|---|---|
| 純粋関数 | 同じ入力なら同じ出力になり、副作用がない | 予測しやすく、テスト可能で、キャッシュ可能 |
| カリー化 | 複数引数を単一引数の連鎖に変換する | 部分適用、合成 |
| 合成 | 小さな関数を組み合わせて大きな関数を作る | 再利用性、モジュール性 |
| Pointfree | 引数に名前を付けずに関数を定義する | ノイズが減り、構造が明確になる |
| 第一級関数 | 値としての関数 | 高階関数、コールバック |
次のステップ
これらの基礎を習得したら、次に進む準備ができています。
- fp-ts Option と Either:関数型のエラー処理
- fp-ts Pipe と Flow:高度な合成パターン
- fp-ts Task と TaskEither:非同期の関数型プログラミング
- モナドとファンクター:fp-ts を支える代数的構造
FP とは、シンプルで合成可能な要素から複雑な振る舞いを構築することです。小さく始め、合成を練習し、コードで役立つことがわかったら、より高度なパターンを少しずつ取り入れてください。