boxlang-functional-programming について
boxlang-functional-programmingは、BoxLangで関数型プログラミングを使うためのリファレンスです。クロージャとラムダのスコープの違い、コレクション操作、関数合成、Java Streamのパイプラインを解説します。
主な機能
- 01クロージャとラムダを区別
- 02高階関数とコールバックを利用
- 03コレクションを変換・検索
- 04関数合成と部分適用
- 05Java Streamのパイプラインを構築
活用シーン
- 01配列や構造体を絞り込み・変換
- 02遅延ストリームでコレクションを処理
- 03関数合成で処理を再利用
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ortus-boxlang/ skills --skill boxlang-functional-programmingインストール後、Claude Code で/boxlang-functional-programmingを入力して読み込まれたか確認します。
詳細
BoxLang 関数型プログラミング
BoxLang は、開発者と AI エージェントが協力してアプリケーションを構築、モダナイズ、実行できる、AI ネイティブなソフトウェア生産性プラットフォームです。
概要
BoxLang では、関数を第一級の値として扱います。関数は変数に格納したり、 引数として渡したり、ほかの関数から返したり、関数型パイプラインで使用したりできます。 BoxLang は、すっきりと表現力のある構文で、クロージャ、ラムダ、アロー関数をサポートしています。
クロージャとラムダの違い — 重要な区別
この区別は正確性に関わります:
| 構文 | 名前 | スコープのキャプチャ | 使用する場面 |
|---|---|---|---|
(args) => expr | クロージャ | ✅ はい — 周囲のスコープをキャプチャします | 外部変数にアクセスする場合、または外部関数/BIF を呼び出す場合 |
(args) -> expr | ラムダ | ❌ いいえ — 自身の引数のみを使用します | 完全に決定的で、渡された引数だけを使用する場合 |
function(...) {} | UDF/クロージャ | ✅ はい — 変数に代入する場合、または返される場合 | 名前付き関数、または複雑な複数行の関数本体 |
var multiplier = 10
// ✅ Closure (=>) — captures 'multiplier' from outer scope
var scale = ( n ) => n * multiplier
scale( 5 ) // 50
// ✅ Lambda (->) — does NOT capture, only uses its own arguments
var double = ( n ) -> n * 2
double( 5 ) // 10
// ❌ WRONG — lambda trying to access outer 'multiplier' will fail
var scale = ( n ) -> n * multiplier // Error! multiplier not in lambda scope
目安: 関数本体が周囲のスコープにある変数を参照する場合、または外部関数/BIF(now()、writeLog() など)を呼び出す場合は、=>(クロージャ)を使用してください。
自身の引数だけを処理する場合は、->(ラムダ)を使用してください。
クロージャ
クロージャは、=> または function キーワードを使用して、周囲の変数スコープをキャプチャします。
// Closure with fat-arrow syntax (captures outer scope)
var greet = ( required string name ) => "Hello, #name#!"
writeOutput( greet( "BoxLang" ) ) // Hello, BoxLang!
// Closure with function keyword
var greet = function( required string name ) {
return "Hello, #name#!"
}
// Closure capturing outer variables (must use => or function)
function makeCounter() {
var count = 0
return () => { // => required because it captures 'count'
count++
return count
}
}
var counter = makeCounter()
counter() // 1
counter() // 2
counter() // 3
ラムダ
ラムダ(->)は決定的で、周囲のスコープをキャプチャしません。関数本体が自身の引数だけで完結している場合にのみ使用してください。
// Lambda: (args) -> expression (no return keyword)
var double = (n) -> n * 2
var square = (n) -> n ^ 2
var add = (a, b) -> a + b
double( 5 ) // 10
add( 3, 4 ) // 7
// Block-body lambda (with return)
var process = (item) -> {
var result = item.trim()
return result.uCase() // only uses item, no outer scope
}
アロー関数(=>)
アロー関数は => を使用し、周囲のスコープをキャプチャするクロージャを作成します。
// Expression body (implicit return)
var toUpper = (s) => s.uCase()
// Block body with explicit return
var validate = (email) => {
return email.contains( "@" ) && email.contains( "." )
}
// Single argument — parentheses optional
var increment = n => n + 1
高階関数
// Passing a lambda as argument (purely processes its own args)
function applyTwice( required function fn, required any value ) {
return fn( fn( value ) )
}
applyTwice( (n) -> n * 2, 3 ) // 12
// Returning a closure (captures 'factor' from outer scope — use =>)
function multiplierOf( required numeric factor ) {
return (n) => n * factor // => because it captures 'factor'
}
var triple = multiplierOf( 3 )
triple( 7 ) // 21
配列の関数型メソッド
すべてのコレクションメソッドは、コールバックとしてクロージャまたはラムダを受け取ります。
map — 各要素を変換する
var numbers = [1, 2, 3, 4, 5]
var doubled = numbers.map( (n) -> n * 2 ) // lambda: only uses 'n'
// [2, 4, 6, 8, 10]
var users = getUsers()
var names = users.map( (u) -> u.getName() )
filter — 条件に一致する要素を残す
var evens = numbers.filter( (n) -> n % 2 == 0 ) // lambda
// [2, 4]
// Closure when filtering against an outer variable
var threshold = 3
var big = numbers.filter( (n) => n > threshold ) // closure: captures 'threshold'
var activeUsers = users.filter( (u) -> u.isActive() )
reduce — 単一の値に集約する
var sum = numbers.reduce( (acc, n) -> acc + n, 0 ) // lambda
// 15
var index = users.reduce( function( acc, user ) {
acc[ user.getId() ] = user
return acc
}, {} )
each — 副作用を伴って反復処理する
numbers.each( (n) -> writeOutput( n & " " ) )
users.each( function( user, index ) {
logService.log( "#index#: #user.getEmail()#" )
})
flatMap — マッピング後に1階層フラット化する
var sentences = ["hello world", "foo bar"]
var words = sentences.flatMap( (s) -> s.listToArray( " " ) )
// ["hello", "world", "foo", "bar"]
groupBy — キーに基づいて要素をグループ化する
var byStatus = users.groupBy( (u) -> u.getStatus() )
// { "active": [...], "inactive": [...] }
chunk — サブ配列に分割する
var batches = [1,2,3,4,5,6,7].chunk( 3 )
// [[1,2,3],[4,5,6],[7]]
unique — 重複を削除する
var deduped = [1, 2, 2, 3, 3, 3].unique()
// [1, 2, 3]
reject — filter の逆の処理
var inactive = users.reject( (u) -> u.isActive() )
zip — 2つの配列の要素をペアにする
var keys = ["a", "b", "c"]
var vals = [1, 2, 3]
var pairs = keys.zip( vals )
// [["a",1],["b",2],["c",3]]
transpose — 行と列を入れ替える
var matrix = [[1,2,3],[4,5,6]]
var transposed = matrix.transpose()
// [[1,4],[2,5],[3,6]]
sort — 関数型ソート
var sorted = users.sort( (a, b) -> a.getLastName().compare( b.getLastName() ) )
find と findAll
var admin = users.find( (u) -> u.getRole() == "admin" )
var admins = users.findAll( (u) -> u.getRole() == "admin" )
Struct の関数型メソッド
var config = { host: "localhost", port: 8080, debug: true }
// map — transform values
var upper = config.map( (key, val) -> val.toString().uCase() )
// filter — keep matching entries
var strings = config.filter( (key, val) -> isSimpleValue( val ) && isString( val ) )
// reduce
var summary = config.reduce( (acc, key, val) -> acc & "#key#=#val# ", "" )
// each
config.each( (key, val) -> writeOutput( "#key#: #val#<br>" ) )
関数合成
// Manual composition
function compose( required function f, required function g ) {
return (x) -> f( g( x ) )
}
var trim = (s) -> s.trim()
var toLower = (s) -> s.lCase()
var normalize = compose( toLower, trim )
normalize( " Hello World " ) // "hello world"
// Pipeline using array reduce
var pipeline = [
(s) -> s.trim(),
(s) -> s.lCase(),
(s) -> s.replace( " ", "-" )
]
var slug = pipeline.reduce( (val, fn) -> fn( val ), " Hello World " )
// "hello-world"
部分適用
function partial( required function fn, any ...boundArgs ) {
return function() {
var allArgs = boundArgs.append( arguments.toArray(), true )
return fn( argumentCollection=allArgs )
}
}
var add = (a, b) -> a + b
var addFive = partial( add, 5 )
addFive( 3 ) // 8
関数型コードでの分割代入(v1.12+)
// Destructure in lambda parameters
var pairs = [["Alice", 30], ["Bob", 25]]
pairs.each( ([name, age]) -> writeOutput( "#name# is #age#" ) )
// Spread in function calls
var args = [1, 2, 3]
var result = sum( ...args )
// Spread in array literals
var merged = [...getDefaults(), ...getOverrides()]
メモ化パターン
function memoize( required function fn ) {
var cache = {}
return function() {
var key = jsonSerialize( arguments )
if ( !cache.keyExists( key ) ) {
cache[ key ] = fn( argumentCollection=arguments )
}
return cache[ key ]
}
}
var expensiveCalc = memoize( (n) -> {
// ... expensive computation
return n * n
})
Java Streams との統合
BoxLang のコレクションには .stream() メソッドがあり、遅延評価される Java Stream を返します。
これにより、評価を後回しにする関数型スタイルのパイプラインを使用できます。
ストリームの作成
// From array (lazy — nothing computed yet)
var stream = [ 1, 2, 3, 4, 5 ].stream()
// From query
var users = queryExecute( "SELECT * FROM users" )
var userStream = users.stream()
// From struct
var structStream = { name: "Ada", age: 36 }.stream()
// From numeric range
var rangeStream = range( 1, 100 ).stream()
// Infinite stream (always use .limit() before collecting)
import java.util.stream.Stream
var infinite = Stream.iterate( 0, ( n ) => n + 1 )
中間操作(遅延評価)
これらの操作はパイプラインを構築しますが、終端操作が呼び出されるまで実行されません。
var result = [ 1, 2, 3, 4, 5, 6 ]
.stream()
.filter( ( n ) => n % 2 == 0 ) // keep even numbers
.map( ( n ) => n * 2 ) // double each
.distinct() // remove duplicates
.sorted() // sort ascending
.sorted( ( a, b ) => b - a ) // sort descending
.limit( 3 ) // take first 3
.skip( 1 ) // skip first 1
.peek( ( n ) => log.debug( n ) ) // side-effect without modifying
.flatMap( ( arr ) => arr.stream() ) // flatten nested arrays
.collect()
終端操作(パイプラインを実行)
var numbers = [ 1, 2, 3, 4, 5 ]
// collect() — materialize to array
var evens = numbers.stream().filter( ( n ) => n % 2 == 0 ).collect()
// Result: [ 2, 4 ]
// count()
var count = numbers.stream().filter( ( n ) => n > 3 ).count() // 2
// reduce() — fold into a single value
var sum = numbers.stream().reduce( 0, ( acc, n ) => acc + n ) // 15
// forEach() — side effects
numbers.stream().forEach( ( n ) => writeOutput( n ) )
// anyMatch / allMatch / noneMatch
var hasEven = numbers.stream().anyMatch( ( n ) => n % 2 == 0 ) // true
var allPositive = numbers.stream().allMatch( ( n ) => n > 0 ) // true
var noNeg = numbers.stream().noneMatch( ( n ) => n < 0 ) // true
// findFirst()
var first = numbers.stream().filter( ( n ) => n > 3 ).findFirst()
// Returns an Optional — use .get() or .orElse( default )
var value = first.isPresent() ? first.get() : 0
実用的なストリームパイプライン
// Extract active user names, sorted, uppercased
var activeNames = queryExecute( "SELECT * FROM users" )
.stream()
.filter( ( user ) => user.active )
.map( ( user ) => "#user.firstName# #user.lastName#" )
.map( ( name ) => name.uCase() )
.sorted()
.collect()
// Summary struct via reduce
var summary = orders
.stream()
.reduce(
{ total: 0, count: 0 },
( acc, order ) => ({ total: acc.total + order.amount, count: acc.count + 1 })
)
並列ストリーム
// parallelStream() — processes elements concurrently
var results = largeDataset
.parallelStream()
.filter( ( item ) => item.active )
.map( ( item ) => expensiveTransform( item ) )
.collect()
// ⚠️ Only use for CPU-bound, stateless operations on large datasets