Skip to content

標準ライブラリ

Onionの標準ライブラリは、一般的な機能のための組み込みモジュールとインターフェースで構成されています。

モジュール一覧

領域 モジュール
I/O・システム IO(コンソール), Files(ファイル・パス), System, Proc(サブプロセス), Args(CLI)
ネットワーク Http(HTTPクライアント), Net(TCPソケット), Server(HTTPサーバ)
データストア Db(JDBC経由のSQL)
アーカイブ Archive(zip・gzip)
並行処理 Future, Concurrent(プール・カウンタ・ロック・チャネル)
コレクション Colls(リスト: map/filter/fold, chunked/windowed, sumBy/maxBy), Iterables, Maps, Sets
テキスト Strings(大小文字・分割・パディング・パース), Text(wrap/indent/table), Regex
数値 Math, OnionMath(双曲線関数, clamp, hypot, 範囲付きrandomInt), Stats(sum/average/median/stddev), Format(桁区切り・bytes・duration)
データ形式 Json, Yaml, Csv, Config(ドット記法での設定値アクセス)
エンコード Codec(base64/hex/url), Hash(md5/sha256/…)
関数型 Option, Result, Future, OutcomeDefect(外部データの読み取り)
位置情報 Origin(値がどのテキストのどこから来たか)
境界 ShapeShapes(テキスト↔型付き値), Scalars
日時・乱数 DateTime, Rand(choice/shuffle/sample/uuid)
テスト・計測 Assert, Timing

ほとんどのヘルパーは静的な Module:: 呼び出しだけでなくメソッドチェインでも書けます—— コレクション(list.filter { ... }.map { ... }, m.mapValues { ... })、文字列 ("s".capitalize())、ハッシュ・エンコード("pw".sha256(), "x".base64Encode())、 テキスト整形(text.wrap(40))、数値集計(nums.sum(), nums.average())、数値フォーマット ((1536L).bytes(), (21L).ordinal())。

IO モジュール

コンソール入出力操作。

IO::println

標準出力に改行付きで出力:

IO::println("Hello, World!")
IO::println("値: " + value)

IO::print

改行なしで出力:

IO::print("名前を入力: ")
val name: String = IO::readln()

IO::readln

ユーザーから1行の入力を読み取り:

val name: String = IO::readln("名前は? ")
IO::println("こんにちは、" + name)

IO::input(prompt) はこの名前で直接呼び出せる同じ操作 -- readln(prompt) は 内部で input を呼び出す実装:

val name: String = IO::input("名前は? ")

IO::readLine

標準入力から1行読み取り、入力の終端では null を返す。プロンプトなしの IO::readln() はこのメソッドの別名:

val line: String? = IO::readLine()

IO::readAll

残りの標準入力全体を1つの文字列として読み取り:

val everything: String = IO::readAll()

フォーマット出力

IO::printf("%s is %d\n", "age", 30)
val s: String = IO::format("%.2f", 3.14159)

エラー出力(stderr)

IO::eprint("warning: ")
IO::eprintln("disk almost full")
IO::eprintf("failed after %d retries\n", 3)

型安全な入力

1行を指定の型として読み取ってパースし、不正な入力なら例外を投げる。各メソッドには 先にプロンプトを表示するオーバーロードがある:

val age: Int = IO::readInt("Age: ")
val price: Long = IO::readLong("Price: ")
val ratio: Double = IO::readDouble("Ratio: ")
val ok: Boolean = IO::readBoolean("Continue? ")  // true/yes/1、false/no/0 を受け付け

安全な入力

上記の型安全な読み取りと同様だが、不正な入力や入力終端では例外を投げず null を返す:

val n: Int? = IO::tryReadInt("N: ")
val d: Double? = IO::tryReadDouble("D: ")
val l: Long? = IO::tryReadLong("L: ")

行単位の入出力

val lines: List = IO::readLines()          // 入力の終端まで読み取り
IO::eachLine { line -> IO::println(line) } // 残りの各行にコールバックを適用
IO::printLines(["a", "b", "c"])            // 1項目1行で出力
IO::printAll("a", "b", "c")                // printLines の可変長引数版

ユーティリティ

IO::flush()    // 標準出力をフラッシュ
IO::newline()  // 空行を出力
IO::clear()    // ターミナル画面をクリア(ANSIエスケープコード)

Math モジュール

JavaのMathクラス経由の数学演算。

Math::random

0.0から1.0の乱数を生成:

val r: Double = Math::random()
val randomInt: Int = (Math::random() * 100) as Int

Math::sqrt

平方根:

val result: Double = Math::sqrt(16.0)  // 4.0

Math::pow

累乗:

val result: Double = Math::pow(2.0, 3.0)  // 8.0

Math::abs

絶対値:

val abs1: Int = Math::abs(-10)     // 10
val abs2: Double = Math::abs(-3.14)  // 3.14

Math::max / Math::min

最大値と最小値:

val max: Int = Math::max(10, 20)    // 20
val min: Int = Math::min(10, 20)    // 10

Math::floor / Math::ceil / Math::round

丸め処理:

val floor: Double = Math::floor(3.7)  // 3.0
val ceil: Double = Math::ceil(3.2)    // 4.0
val round: Long = Math::round(3.5)    // 4

Math::sin / Math::cos / Math::tan

三角関数(ラジアン):

val sine: Double = Math::sin(Math::PI / 2)    // 1.0
val cosine: Double = Math::cos(0.0)           // 1.0
val tangent: Double = Math::tan(Math::PI / 4) // 1.0

Math Constants

val pi: Double = Math::PI       // 3.14159...
val e: Double = Math::E         // 2.71828...

OnionMath モジュール

JDKのMathとは別のonion.*数値モジュールで、双曲線関数、安全な丸め・クランプ、範囲付きの 乱数整数を提供します。標準ライブラリの他のモジュールと同様デフォルトでインポートされるため、 明示的なインポートは不要です。

OnionMath::sin / OnionMath::cos / OnionMath::tan / OnionMath::asin / OnionMath::acos / OnionMath::atan / OnionMath::atan2

三角関数と逆三角関数(ラジアン):

val sine: Double = OnionMath::sin(OnionMath::PI / 2)     // 1.0
val angle: Double = OnionMath::atan2(1.0, 1.0)           // pi/4

OnionMath::sinh / OnionMath::cosh / OnionMath::tanh

双曲線関数:

val h: Double = OnionMath::sinh(1.0)

OnionMath::exp / OnionMath::log / OnionMath::log10

指数関数と対数関数:

val e2: Double = OnionMath::exp(1.0)     // e
val l: Double = OnionMath::log(OnionMath::E)   // 1.0
val l10: Double = OnionMath::log10(100.0)      // 2.0

OnionMath::pow / OnionMath::sqrt / OnionMath::cbrt

累乗と平方根・立方根:

val cube: Double = OnionMath::pow(2.0, 3.0)  // 8.0
val root: Double = OnionMath::sqrt(16.0)     // 4.0
val croot: Double = OnionMath::cbrt(27.0)    // 3.0

OnionMath::abs / OnionMath::absFloat / OnionMath::absInt / OnionMath::absLong

プリミティブ型ごとの絶対値:

val a1: Double = OnionMath::abs(-3.14)
val a2: Int = OnionMath::absInt(-10)      // 10
val a3: Long = OnionMath::absLong(-10L)   // 10

OnionMath::min / OnionMath::minInt / OnionMath::minLong / OnionMath::max / OnionMath::maxInt / OnionMath::maxLong

プリミティブ型ごとの最小値・最大値:

val lo: Int = OnionMath::minInt(10, 20)   // 10
val hi: Int = OnionMath::maxInt(10, 20)   // 20

OnionMath::floor / OnionMath::ceil / OnionMath::round / OnionMath::roundFloat

丸め処理:

val f: Double = OnionMath::floor(3.7)     // 3.0
val c: Double = OnionMath::ceil(3.2)      // 4.0
val r: Long = OnionMath::round(3.5)       // 4
val rf: Int = OnionMath::roundFloat(3.5f) // 4

OnionMath::random / OnionMath::randomInt

乱数生成。Math::randomと異なり、randomIntは範囲を直接指定でき、エフェクトチェッカーに よってRandエフェクトとして追跡されます:

val r: Double = OnionMath::random()          // [0.0, 1.0)
val n: Int = OnionMath::randomInt(1, 10)     // [1, 10](両端含む)

OnionMath::signum / OnionMath::signumFloat

数値の符号(-1.00.0、または1.0):

val s: Double = OnionMath::signum(-5.0)   // -1.0

OnionMath::toRadians / OnionMath::toDegrees

角度単位の変換:

val rad: Double = OnionMath::toRadians(180.0)  // pi
val deg: Double = OnionMath::toDegrees(OnionMath::PI)  // 180.0

OnionMath::clamp / OnionMath::clampInt

値を範囲内に収める:

val c1: Double = OnionMath::clamp(15.0, 0.0, 10.0)  // 10.0
val c2: Int = OnionMath::clampInt(-5, 0, 10)        // 0

OnionMath::hypot

オーバーフロー・アンダーフローを避けた斜辺の長さ:

val h: Double = OnionMath::hypot(3.0, 4.0)  // 5.0

OnionMath Constants

val pi: Double = OnionMath::PI  // 3.14159...
val e: Double = OnionMath::E    // 2.71828...

Origin

値がどのテキストのどこから来たかを表します。コンパイラが持つソース位置の、実行時版です。 12行目で失敗したと分かっているパーサが、null を返す代わりにそう言えるようになります。

source は自由形式です——ファイルパス、URL、"<stdin>""<literal>" など。行と列は 1始まりです。列が 0 の場合は「行までしか分からない」ことを意味します。行単位のパーサが 正直に報告できるのはそこまでだからです。

import { onion.Origin; }

val o = Origin::at("access.log", 12, 5)
println(o.describe())          // access.log:12:5

val lineOnly = Origin::atLine("data.json", 4)
println(lineOnly.describe())   // data.json:4
println(lineOnly.hasColumn())  // false

Origin::at / Origin::atLine / Origin::spanning

at(source, line, column) は1文字分、atLine(source, line) は列なしの行、 spanning(source, line, column, span)span 文字分を表します。

origin.onLine / origin.inSource

文書を行単位でパースすると、各行のパース結果はその行を基準にした位置を報告します。 onLine はそれを文書全体の位置に持ち上げ、inSource は別のソースに付け替えます。

Origin::at("log.txt", 1, 3).onLine(40).describe()   // log.txt:40:3

origin.describe

file:line:column、行しか分からない場合は file:line を返します。コンパイラもエディタも 既に解釈できる形式です。toString も同じ結果を返します。

Outcome と Defect

外部データを読んだ結果です。値か、あるいは読めなかった理由すべてを表します。 Defect は「1つの不具合」、Outcome[T] は「値、またはその一覧」です。

Defect は呼び出し側が実際に知りたい3点に答えます——テキストのどこか(origin、無い場合 もある)、値のどこか(path)、そして何を期待して何があったか。

import { onion.Outcome; onion.Defect; onion.Origin; }

val d = Defect::at(Origin::atLine("config.json", 4), "port", "Int", "\"http\"")
println(d.describe())     // config.json:4: port: expected Int, found "http"

val missing = Defect::of("name", "String", "absent")
println(missing.describe())   // name: expected String, found absent

なぜ Result ではないのか

zip のためです。Result はモナドで bind は短絡するため、最初の不正フィールドが残りを 隠してしまいます。3つのフィールドが同時に壊れているなら、1回で3件報告すべきです。

Ok(f)   zip Ok(x)   = Ok(f(x))
Bad(d1) zip Ok(_)   = Bad(d1)
Ok(_)   zip Bad(d2) = Bad(d2)
Bad(d1) zip Bad(d2) = Bad(d1 ++ d2)     <- この型が存在する理由
val a: Outcome[JInteger] = Outcome::bad(Defect::of("x", "Int", "p"))
val b: Outcome[JInteger] = Outcome::bad(Defect::of("y", "Int", "q"))
println(a.zip(b) { p, q -> p + q }.defects().size)   // 1 ではなく 2

bind は短絡したままです——後続の計算が前の値に依存しうる以上、そうでなければなりません。 両方使えます。do[Outcome]bind を使います。

まとめて読む

all は全部か無かで、全 defect を集約します。部分的な結果に価値がある場合——良い行が 意味を持つログファイルなど——は valuesdefects で両方を取れます。

val os: List[Outcome[JInteger]] =
  [Outcome::ok(1), Outcome::bad(Defect::of("a", "Int", "x")), Outcome::ok(3)]

println(Outcome::values(os).size)    // 2
println(Outcome::defects(os).size)   // 1
println(Outcome::all(os).isOk())     // false

ネストや行単位の読み取り

under は各 defect の path に接頭辞を付け、onLine はある行を基準に報告された位置を 文書全体の位置に持ち上げます。

o.under("address")     // "city" が "address.city" になる
o.onLine(40)           // 断片の1行目の defect が、ファイルの40行目になる

Shape

外部テキストと型付き値の、部分的かつ(可能なら)双方向の対応です。Shape[T] はテキストを T として読み、対応が可逆なら書き戻せます。

import { onion.Shape; onion.Shapes; onion.Outcome; }

val r = pointShape.parse("3,4")
if r.isOk() { println(r.get()) }
println(pointShape.print(pt))

意図的に区別している2つの法則

L1  往復    parse(print(v)) == Ok(v)     print がある限り保証される
L2  正規化  print(parse(t)) == t         一般には成り立たない

L2 が破れるのはごく普通の理由です——"007" は正しい Int ですが、書き戻すと "7" になります。 L2 も満たす shape を lossless と呼び、これは稀で、lens が必要とするものです。多くの shape は L1 のみで、どちらなのかを明言することが「可逆な言語」と「可逆だと主張する言語」の差です。

canPrint

すべての shape が書き戻せるわけではありません。\s+ を区切りに使う正規表現には一意な 書き戻し方が無いため read-only になり、canPrint()print を呼ぶ前にそう伝えます—— メソッドが黙って存在しない、という形にはしません。

成分の失敗は蓄積する

"abc,def" から Int を2つ読むと defect は2件報告されます。最初の1件ではありません。 Outcome の蓄積する zip はそのためにあります。

Lossless shape と lens

L2 も満たす shape を lossless と呼びます——isLossless() がそれを伝え、 parseLossless(text[, origin]) は素の T の代わりに Lossless[T] を読みます:値と、 その周辺すべての Residue(コメント、空白、キーの順序、元の値の書き方)です。 printLossless(value, residue) はその residue を通して書き戻します——変更していない 部分はバイト単位で再現され、意図的に変更した値だけが書き直されます。Residue は 不透明な値で、生成した shape にだけ渡し戻してください。

Lossless[T] そのものが lens です:value()/residue() でペアを読み、 withValue(v) は residue を保ったまま値だけ差し替え、edit { v -> ... } は更新を 値にフォーカスします。render() がテキストを再構成します:

val r   = configShape.parseLossless(file"app.conf".text()).get()
val out = r.edit { v -> v.copy(port = 9090) }.render()
// diff app.conf out  ->  1行だけ変わる

Shapes::configShapes::yaml は、shape name = config / shape name = yaml の 糖衣構文の裏にある lossless shape を、Shape[T] の値として直接組み立てます。

コンビネータ

  • eachLine(text[, origin]) — 1行ごとに Outcome[T] を返し、読めた行と読めなかった 行の defect の両方を保持します(Outcome::values/Outcome::defects で分離できます)。 ログファイルのように大半の行が読めるケースなど、部分的な結果に意味がある場合は lines() よりこちらを使います。
  • lines() — 1行1値、全部読めるか失敗するかの Shape[List[T]] です。
  • sepBy(separator) — リテラルな区切り文字で分割する、全部読めるか失敗するかの Shape[List[T]] です。
  • xmap(forward, backward) — 同型写像に沿って shape を運びます。print が黙って 壊れないよう、両方向の関数が必要です。
  • orElse(other) — この shape、読めなければ other。どちらも読めない場合は両方の defect を報告します。印字はこの shape で行います。

Scalars モジュール

境界(外部データを読み込む場所)向けの、厳格なスカラー変換です。record ... from re"..."shape が生成するコードから使われるほか、直接呼び出すこともできます。 JDK 自身のパーサが緩すぎてそのままでは使えない場面のためのものです。

なぜ Boolean::parseBoolean ではないのか

java.lang.X.parseX はどれも不正な入力を例外で拒否します -- Boolean::parseBoolean だけが例外で、"true" 以外のすべてを false に変換してしまいます。これはパーサが 絶対にやってはいけない失敗の仕方です。"maybe""yes""1" がすべて false に なり、データが不正だったことを示すものが何も残りません。

Scalars::toBoolean("TRUE")     // true
Scalars::toBoolean("false")    // false
Scalars::toBoolean("yes")      // IllegalArgumentException が発生
Scalars::isBoolean("yes")      // false -- toBoolean を呼ぶ前に確認できる

toBooleanIllegalArgumentException を投げます。これは数値パーサが投げる NumberFormatException の親クラスなので、派生コードは両方を同じ方法で捕捉できます。

Scalars::read

texttag で指定したスカラー種別(StringIntLongDoubleFloatBooleanShortByte のいずれか)として読み取り、例外ではなく位置情報付きの Defect として報告します。

import { onion.Scalars; onion.Outcome; }

val port: Outcome[Object] = Scalars::read("Int", "8080", null, "port")
println(port.get())                                    // 8080

val bad: Outcome[Object] = Scalars::read("Int", "http", null, "port")
println(bad.defects().get(0).describe())                // port: expected Int, found "http"

originOrigin または null)は defect をソーステキスト上の位置に結び付け、 path は構築中の値のどこに該当するフィールドかを示します。

Scalars::coerce

Json/Yaml などで既にパース済みのドキュメント値を、tag で指定したスカラー種別に 変換します。read と異なり値は既に型付きで渡ってくるため -- JSON の数値はすでに Number になっている -- これはパースではなく絞り込みです。形そのものが違う値 (Int が必要な場所に文字列がある等)は、黙って null になるのではなく defect に なります。

Scalars::coerce("Int", 8080, null, "port")        // Outcome::ok(8080)
Scalars::coerce("Int", "8080", null, "port")      // Outcome::ok(8080) -- 数値文字列も読める
Scalars::coerce("Int", [1, 2], null, "port")      // defect: expected Int, found an array

readcoerce はどちらもコンパイラ自身のスカラー変換テーブルと同じタグの語彙を 使うため、shape/from re"..." による導出と Scalars を直接使う手書きコードは、 同じ方法で defect を報告します。

関数インターフェース

ラムダとクロージャのための組み込み関数型。f(args)の代わりにf(args)として呼び出せます。

Function0

パラメータなしの関数:

val func: Function0[Int] = () -> { return 42; }
val result: Int = func()

Function1

1パラメータの関数:

val double: Function1[Int, Int] = (x: Int) -> { return x * 2; }
val result: Int = double(5)

Function2

2パラメータの関数:

val add: Function2[Int, Int, Int] = (x: Int, y: Int) -> { return x + y; }
val result: Int = add(3, 7)

Function3 から Function10 まで

3〜10パラメータの関数も同じパターンです。

ラッパークラス

プリミティブ型に対応するJavaのラッパークラス(文脈によってはJ接頭辞でアクセス)。

JInteger

Integer操作:

val i: Int = JInteger::parseInt("42")
val s: String = JInteger::toString(42)
val max: Int = JInteger::MAX_VALUE
val min: Int = JInteger::MIN_VALUE

JLong

Long操作:

val l: Long = JLong::parseLong("1234567890")
val s: String = JLong::toString(1234567890L)

JDouble

Double操作:

val d: Double = JDouble::parseDouble("3.14")
val s: String = JDouble::toString(3.14)

JBoolean

Boolean操作:

val b: Boolean = JBoolean::parseBoolean("true")
val s: String = JBoolean::toString(true)

よく使うJavaクラス

よく使われるJava標準ライブラリのクラス。

String

文字列操作(自動的に利用可能):

val text: String = "Hello, World!"
val upper: String = text.toUpperCase()
val lower: String = text.toLowerCase()
val length: Int = text.length()
val sub: String = text.substring(0, 5)
val contains: Boolean = text.contains("World")
val starts: Boolean = text.startsWith("Hello")
val ends: Boolean = text.endsWith("!")

StringBuilder

効率的な文字列構築:

import { java.lang.StringBuilder; }

val builder: StringBuilder = new StringBuilder()
builder.append("Hello")
builder.append(" ")
builder.append("World")
val result: String = builder.toString()

ArrayList

動的配列:

import { java.util.ArrayList; }

val list: ArrayList[String] = new ArrayList[String]
list.add("First")
list << "Second"  // <<演算子を使用
val size: Int = list.size()
val item: Object = list.get(0)
list.remove(0)
val empty: Boolean = list.isEmpty()

HashMap

キーバリューマップ:

import { java.util.HashMap; }

val map: HashMap[String, String] = new HashMap[String, String]
map.put("key1", "value1")
map.put("key2", "value2")
val value: Object = map.get("key1")
val has: Boolean = map.containsKey("key1")
val size: Int = map.size()

File

ファイル操作:

import { java.io.File; }

val file: File = new File("data.txt")
val exists: Boolean = file.exists()
val isFile: Boolean = file.isFile()
val isDir: Boolean = file.isDirectory()
val name: String = file.getName()
val path: String = file.getPath()
val length: Long = file.length()

BufferedReader

テキストの読み取り:

import {
  java.io.BufferedReader;
  java.io.FileReader;
}

val reader: BufferedReader = new BufferedReader(
  new FileReader("file.txt")
)

var line: String = null
while (line = reader.readLine()) != null {
  IO::println(line)
}

reader.close()

BufferedWriter

テキストの書き込み:

import {
  java.io.BufferedWriter;
  java.io.FileWriter;
}

val writer: BufferedWriter = new BufferedWriter(
  new FileWriter("output.txt")
)

writer.write("Hello, World!")
writer.newLine()
writer.close()

Rand モジュール

onion.Randによる乱数生成ユーティリティ。

Rand::nextInt / nextLong / nextDouble / nextBoolean

乱数を生成:

val randomInt: Int = Rand::nextInt()            // ランダムなInt
val randomLong: Long = Rand::nextLong()         // ランダムなLong
val randomDouble: Double = Rand::nextDouble()   // 0.0から1.0
val randomBool: Boolean = Rand::nextBoolean()   // ランダムなBoolean

Rand::nextInt(範囲指定)

範囲内の乱数整数を生成:

val dice: Int = Rand::nextInt(6) + 1      // 1から6
val percent: Int = Rand::nextInt(100)     // 0から99
val d20: Int = Rand::nextInt(1, 21)       // 1から20(min, 排他的max)

Rand::nextLong(範囲指定)

範囲内の乱数longを生成:

val bigId: Long = Rand::nextLong(1000000L)   // 0から999999

Rand::nextDouble(範囲指定)

val small: Double = Rand::nextDouble(10.0)         // 0.0から10.0
val ranged: Double = Rand::nextDouble(1.0, 2.0)    // 1.0から2.0

Rand::choice

リストからランダムに1要素を選ぶ:

val colors: List[String] = ["red", "green", "blue"]
val picked: String = Rand::choice(colors)

Rand::shuffle

リストをその場でシャッフル:

import { java.util.ArrayList; }

val list: ArrayList[String] = new ArrayList[String]()
list.add("A")
list.add("B")
list.add("C")
Rand::shuffle(list)  // その場でシャッフル

Rand::sample

リストから重複なくn個の要素をランダムに選ぶ:

val deck: List[String] = ["A", "B", "C", "D", "E"]
val hand: List[String] = Rand::sample(deck, 3)   // 重複しない3枚

Rand::uuid

ランダムなUUID文字列を生成:

val id: String = Rand::uuid()   // 例: "3fa85f64-5717-4562-b3fc-2c963f66afa6"

Assert モジュール

onion.Assertによるテストアサーション。失敗時にAssertionErrorをスロー。

基本アサーション

Assert::isTrue(x > 0)
Assert::isFalse(list.isEmpty())
Assert::equals(expected, actual)
Assert::notEquals(a, b)

Nullアサーション

Assert::notNull(result)
Assert::isNull(errorMessage)

明示的な失敗

if invalidState {
  Assert::fail("ここに到達すべきではない")
}

Timing モジュール

onion.Timingによる時間計測ユーティリティ。

現在時刻の取得

val startNanos: Long = Timing::nanos()     // 高精度 (System.nanoTime)
val startMillis: Long = Timing::millis()   // 壁時計 (System.currentTimeMillis)

経過時間の計測

val start: Long = Timing::nanos()
// ... 何らかの処理 ...
val elapsedNs: Long = Timing::elapsedNanos(start)      // ナノ秒での経過時間
val elapsedMs: Double = Timing::elapsedMs(start)       // ミリ秒での経過時間(サブミリ秒精度のdouble)
val elapsedMillis: Long = Timing::elapsedMillis(start) // Timing::millis()起点のミリ秒での経過時間

時間のフォーマット

val nanos: Long = 1234567890L
val formatted: String = Timing::formatNanos(nanos)   // "1.23s"
// 出力形式: "123ns", "45.67μs", "12.34ms", "1.23s"

val millis: Long = 125000L
val formattedMs: String = Timing::formatMillis(millis)  // "2m5s"
// 出力形式: "500ms", "1.23s", "2m30s"

スリープ

Timing::sleep(1000L)        // 1000ミリ秒スリープ
Timing::sleepNanos(500000L) // 500,000ナノ秒スリープ

関数実行時間の計測

// 実行時間を計測して表示し、結果を返す
val result: Int = Timing::measure(() -> { return expensiveOperation(); })
// 出力: "Elapsed: 123.45ms"
val result2: Int = Timing::measure("task", () -> { return expensiveOperation(); })
// 出力: "task: 123.45ms"

// 戻り値のない関数版
Timing::measureVoid(() -> { expensiveOperation(); })
// 出力: "Elapsed: 123.45ms"
Timing::measureVoid("task", () -> { expensiveOperation(); })
// 出力: "task: 123.45ms"

// 表示なしで実行時間(ナノ秒)を取得
val timeNanos: Long = Timing::time(() -> { return expensiveOperation(); })

Option モジュール

onion.Optionで提供。

  • Option::some(value) / Option::none() / Option::of(value)
  • opt.isDefined() / opt.isEmpty() / opt.get()get()None の場合 NoSuchElementException を投げる
  • opt.getOrElse(defaultValue) / opt.orElseGet(() -> default) / opt.orNull()
  • opt.orElseThrow() / opt.orElseThrow(() -> customException)
  • opt.orElse(otherOption)
  • opt.map(f) / opt.flatMap(f) / opt.filter(predicate) / opt.forEach(action)
  • opt.contains(value) / opt.exists(predicate)
  • opt.fold(() -> ifEmpty, v -> ifPresent) — 単一の値へ畳み込む
  • opt.toList() — 0個または1個の要素のリスト

Result モジュール

onion.Resultで提供。

  • Result::ok(value) / Result::err(error)
  • Result::ofNullable(value, errorIfNull) / Result::trying(operation)
  • res.isOk() / res.isErr() / res.get() / res.getError()get()Err で、getError()Ok で例外を投げる
  • res.map(f) / res.mapError(f) / res.flatMap(f) / res.toOption()
  • res.getOrElse(default) / res.orElseGet(() -> default) / res.orNull()
  • res.getOrThrow() / res.getOrThrow(e -> customException) — エラーを(Throwable でなければラップして)投げる、またはマッピングした例外を投げる
  • res.forEach(action) / res.forEachError(action)
  • res.fold(e -> ifErr, v -> ifOk) — 単一の値へ畳み込む
  • res.recover(e -> value) / res.recoverWith(e -> otherResult)Err を回復
  • res.exists(predicate) / res.toList()

Future モジュール

onion.Futureで提供。非同期計算を表現。

Futureの作成

// 値で完了済み
val done: Future[Int] = Future::successful(42)

// 失敗で完了済み
val fail: Future[Int] = Future::failed(new RuntimeException("error"))

// バックグラウンドスレッドで非同期実行
val async: Future[String] = Future::async(() -> { return compute(); })

// 例外処理付きの非同期実行
val safe: Future[Int] = Future::asyncThrowing(() -> {
  return riskyOperation();
})

// 遅延
val delayed: Future[Void] = Future::delay(1000L)  // 1秒

変換メソッド

val f: Future[Int] = Future::successful(10)

// 値を変換
f.map((x: Int) -> { return x * 2; })  // Future[Int] = 20

// 非同期操作をチェイン
f.flatMap((x: Int) -> { return Future::successful(x + 1); })

// フィルタ(述語が false なら失敗)
f.filter((x: Int) -> { return x > 0; })

// flatMap の別名(do記法が使用)
f.bind((x: Int) -> { return Future::successful(x); })

エラーハンドリング

val f: Future[Int] = Future::failed(new RuntimeException("oops"))

// 値で回復
f.recover((e: Throwable) -> { return 0; })

// 別の Future で回復
f.recoverWith((e: Throwable) -> { return Future::successful(42); })

// エラーを変換
f.mapError((e: Throwable) -> { return new CustomException(e); })

コールバック

val f: Future[String] = Future::async(() -> { return "result"; })

f.onSuccess((value: String) -> { IO::println(value); })
f.onFailure((error: Throwable) -> { IO::println(error); })
f.onComplete(
  (value: String) -> { IO::println("ok: " + value); },
  (error: Throwable) -> { IO::println("err: " + error); }
)

ブロッキング操作

val f: Future[Int] = Future::successful(42)

f.await()              // ブロックして結果を取得(失敗時は例外)
f.awaitTimeout(5000L)  // タイムアウト付きでブロック(ミリ秒)
f.getOrElse(0)         // 結果を取得、失敗時はデフォルト値

ステータス照会

f.isCompleted()  // 完了していれば true(成功・失敗いずれも)
f.isSuccess()    // 成功で完了していれば true
f.isFailure()    // エラーで完了していれば true

これらは非ブロッキングです——future の現在の状態を報告するだけなので、まだ実行中の future は isSuccess()isFailure() の両方が false を返します。結果を待ちたい場合は、 isFailure() をポーリングするのではなく await()/getOrElse()(あるいは onSuccess/onFailure/recover)を使ってください。

Futureの結合

val f1: Future[Int] = Future::successful(1)
val f2: Future[Int] = Future::successful(2)

// タプルのような配列へまとめる
f1.zip(f2)  // Future[List[Object]] = [1, 2]

// レース: 先に完了した方が勝つ
f1.race(f2)

// すべての完了を待つ
Future::all(f1, f2, f3)  // Future[List[Object]] = [1, 2, 3]

// 最初に完了したもの
Future::first(f1, f2, f3)

変換

val f: Future[Int] = Future::successful(42)

f.toOption()   // Option[Int] - Some(42) または None(ブロックする)
f.toResult()   // Result[Int, Throwable](ブロックする)
f.underlying() // 相互運用のための Java CompletableFuture

// 逆方向: Java の CompletableFuture を Future でラップする
val cf: java.util.concurrent.CompletableFuture[Int] = someJavaApi()
val wrapped: Future[Int] = Future::fromCompletableFuture(cf)

Do記法サポート

Futureは順次非同期合成のためのdo記法で動作:

val result: Future[Int] = do[Future] {
  x <- Future::async(() -> { return fetchA(); })
  y <- Future::async(() -> { return fetchB(x); })
  ret x + y
}

Json モジュール

JSON のパースとシリアライズ。中間表現は Java の Map / List / scalar(String / Long / Double / Boolean / null)です。

Json::parse / Json::stringify

val obj = Json::parse("{\"name\":\"ko\",\"age\":3}")   // Object(実体は Map)
val name = Json::getString(obj, "name")                // "ko"
val age = Json::getInt(obj, "age")                     // 3

val m = Json::object()                                  // 空の Map
m.put("x", 1)
val text = Json::stringify(m)                           // {"x":1}
val pretty = Json::stringifyPretty(m)                   // インデント付きで整形
val a = Json::array()                                   // 空の List(JSON 配列値の構築に使う)

getString / getInt / getLong / getDouble / getFloat / getBoolean / getShort / getByte でキーから型別に取得します(見つからない・型不一致のときは null)。

これらはボックス化された値を返すため、見つからない場合の null をそのまま非 null なプリミティブへ代入すると NullPointerException になります。getStringOr / getIntOr / getLongOr / getDoubleOr / getFloatOr / getBooleanOr(obj, key, default) はフォールバック値付きでプリミティブを返すので、キーが無くても NPE になりません:

val obj = Json::parse("{}")
Json::getIntOr(obj, "missing", 42)      // 42(NPE にならない)
Json::getStringOr(obj, "name", "anon")  // "anon"

Json::value(ナビゲート可能なラッパー)

Json::value(text)[] でインデックスアクセスできるラッパー値を返します。オブジェクトのキーには文字列、 配列の要素には整数でアクセスでき、値の取り出しには asString() / asInt() / asLong() / asDouble() / asBoolean() を使います:

val v = Json::value(jsonText)
v["users"][0]["name"].asString()

キーが存在しない・添字が範囲外のときは null を保持する Value を返すので、途中の欠損があっても例外にはなりません (末尾で asString() 等を呼ぶと null になります)。isNull() で null かどうか、size() で配列・オブジェクトの 要素数(それ以外は 0)を調べられ、raw() で内部表現(Map/List/scalar/null)を直接取り出せます。

Json::parseOrNull(json)Json::parse(json) と同じですが、不正な入力に対して例外 Json.JsonParseException を投げる代わりに null を返します。パース失敗を別扱いのエラーではなく 単なる「値が無い」ケースとして扱いたいときに便利です:

val obj = Json::parseOrNull("not json")   // 例外を投げず null

不正入力の失敗をその場で処理したい場合、Json.JsonParseException は通常の message() に加えて getPosition()(パースを諦めた位置の文字オフセット)を持っています:

try {
  Json::parse("{bad json")
} catch e: Json.JsonParseException {
  IO::println(e.message() + " at offset " + e.getPosition())
}

Json::asObject(obj)Json::asArray(obj) は素の Map/List 表現に対する型安全なキャストです。 実行時の型が一致していれば Map/List にキャストした値を、そうでなければ null を返します。 Json::getJson::parseJson::parseOrNullObject を返した後、Map/List として イテレートしたいときに使います:

val obj = Json::parse("{\"tags\": [\"a\", \"b\"]}")
val tags = Json::asArray(Json::get(obj, "tags"))   // List。"tags" が配列でなければ null

Yaml モジュール

flat block mapping ドキュメント限定の YAML パースとシリアライズ(onion.Yaml)。 Json と同じ中間表現を共有しており(scalar は同じ Java 型にマップされる)、 derive!(Yaml)derive!(Json) とまったく同じ toMap / fromMap の土台の上に 構築されています。

対象範囲: flat block mapping のみ(ネストした map、シーケンス、アンカーは非対応)。

Yaml::parse

YAML の flat block-mapping 文字列を LinkedHashMap にパースします:

val data = Yaml::parse("name: Alice\nage: 30\n")
// data は LinkedHashMap;scalar の型推論は Json::parse と同じ

scalar の型推論規則(Json と同一): - "" または nullnull - true / falseBoolean - 整数リテラル(-?\d+ にマッチ)→ Long - 浮動小数点パターンや ./e/E を含む数値 → Double - クォートされた "..."String(エスケープ解除のみ、それ以上の変換なし) - それ以外 → String

不正な入力に対しては Yaml.YamlParseException を投げます。derive!(Yaml)fromYaml はこれを捕捉して代わりに null を返します。

不正入力の失敗をその場で処理したい場合、Yaml.YamlParseException は通常の message() に加えて getLine()(パースを諦めた行番号、1始まり)を持っています:

try {
  Yaml::parse("no colon here")
} catch e: Yaml.YamlParseException {
  IO::println(e.message() + " at line " + e.getLine())
}

Yaml::stringify

Map(または scalar)を YAML の flat block-mapping 文字列にシリアライズします:

val m = ["name": "Alice", "age": 30L]
val yaml = Yaml::stringify(m)
// "name: Alice\nage: 30\n"

パースし直したときに誤読される可能性のある文字列値(:#、改行を含む、または 数値・真偽値に見えるもの)は自動的にダブルクォートされます。数値と真偽値はそのまま 出力されます。Map のキーも同じ規則でクォートされます — : や前後の空白を含む キーは key: value の区切りと衝突しないようダブルクォートされます。

round-trip の保証

Yaml::parse が生成した任意の Map について、Yaml::parse(Yaml::stringify(m)) は 等しい map を返します。同様に、derive!(Yaml) を付けたレコードでは、scalar 成分の みを持つすべての値について fromYaml(toYaml(v)) == v が成り立ちます。

derive!(Yaml) の利用

derive!(Yaml) は scalar 成分のみを持つ任意のレコードに対して fromYamltoYaml を合成します。

record ServerConfig(host: String, port: Int, debug: Boolean) derive!(Yaml)

val cfg = new ServerConfig("localhost", 8080, false)
val yaml = ServerConfig::toYaml(cfg)
// "host: localhost\nport: 8080\ndebug: false\n"

val cfg2 = ServerConfig::fromYaml(yaml)   // ServerConfig? — パース/変換失敗時は null

derive!(Json, Yaml) も有効です。両フォーマットは内部の toMap / fromMap を 共有するため、重複はありません:

record User(name: String, age: Int) derive!(Json, Yaml)

val u = new User("ko", 3)
val viaJson = User::fromJson(User::toJson(u))   // == u
val viaYaml = User::fromYaml(User::toYaml(u))  // == u

Config モジュール

パース済み JSON に対するドット記法アクセスと設定読み込み(onion.Config)。内部では Json::parse をそのまま使うので、object / array / scalar の形は Json と同じです。YAML や .env 形式には対応せず、あくまで JSON とドット区切りパス、環境変数によるオーバーライドを提供します。

val config = Config::loadJson("config.json")          // ファイルを読んでパース
val config2 = Config::parseJson("{\"port\": 8080}")   // JSON 文字列を直接パース

Config::get(config, "database.host")                   // 生の値、見つからなければ null
Config::getString(config, "database.host", "localhost")
Config::getInt(config, "database.port", 5432)
Config::getLong(config, "database.maxConnections", 10L)
Config::getDouble(config, "database.timeout", 30.0)
Config::getBoolean(config, "database.ssl", false)

パスはドット区切りで、object と array の両方をたどれます。数字のセグメントは array のインデックスとして扱われます:

val config = Config::parseJson("{\"users\": [{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]}")
Config::getString(config, "users.0.name", "unknown")   // "Alice"

キーが見つからない場合・array の添字が範囲外の場合・値を要求された型へ変換できない場合は、例外を投げず指定したデフォルト値にフォールバックします。数値系のゲッターは JSON の数値だけでなく数値文字列も受け付けます。hasPath はデフォルト値なしで存在確認だけ行います:

Config::hasPath(config, "database.host")   // true / false

環境変数へのアクセスも用意されています。getEnv はそのまま環境変数を読み、getWithEnvOverride は設定パスを読みつつ、対応する環境変数がセットされていればそちらを優先します(デプロイ時に設定ファイルの値を上書きするのに便利です):

Config::getEnv("PORT", "3000")
Config::getWithEnvOverride(config, "database.host", "DB_HOST", "localhost")

Strings モジュール

文字列ユーティリティ(onion.Strings、自動 import):

Strings::split("a,b,c", ",")          // List[String] ["a","b","c"]
Strings::splitRegex("a1b2c", "[0-9]") // List[String] ["a","b","c"]
Strings::join(parts, "-")             // 配列・List どちらも可
Strings::upper(s) / Strings::lower(s) / Strings::trim(s)
Strings::replace(s, "a", "b") / Strings::replaceRegex(s, "[0-9]+", "#")
Strings::startsWith(s, p) / Strings::endsWith(s, p) / Strings::contains(s, sub)
Strings::padLeft(s, 8, '0') / Strings::padRight(s, 8, ' ') / Strings::repeat(s, 3)

大文字小文字と検査のヘルパー:

Strings::capitalize("hello")             // "Hello"
Strings::decapitalize("Hello")           // "hello"
Strings::capitalizeWords("a b c")        // "A B C"
Strings::containsIgnoreCase(s, sub) / Strings::equalsIgnoreCase(a, b)
Strings::count("banana", "a")            // 3
Strings::isEmpty("") / Strings::isBlank("   ")   // true / true
Strings::reverse("abc")                  // "cba"
Strings::lines("a\nb\r\nc")              // List[String] ["a","b","c"]
Strings::removePrefix("unhappy", "un")   // "happy"
Strings::removeSuffix("running", "ing")  // "runn"
Strings::truncate("hello world", 8, "...")   // "hello..."
Strings::center("hi", 6, '*')            // "**hi**"
Strings::ifBlank("   ", "default")       // "default"
Strings::words("  a  b  c ")             // List[String] ["a","b","c"]
Strings::chars("abc")                    // List ["a","b","c"]
Strings::substring("hello", 1) / Strings::substring("hello", 1, 3)  // "ello" / "el"
Strings::indexOf("hello", "l") / Strings::lastIndexOf("hello", "l")   // 2 / 3
// null 安全なパース(例外を投げずに null/フォールバックを返す)
Strings::toIntOrNull("42") / Strings::toLongOrNull("100") / Strings::toDoubleOrNull("3.14")
Strings::toIntOr("nope", 0)              // 0

Strings の大半のメソッド(uppertrimstartsWithindexOfcapitalize など)は拡張メソッドのメソッドチェーン(s.upper()s.trim() など)としても静的呼び出しと同じ挙動で使えます。例外は splitsubstringlinescharsrepeat の5つです。 java.lang.String にはすでに同名のメソッドが定義されており、同名の インスタンスメソッドは常に拡張メソッドより優先されるため、 s.split(",")s.substring(1)s.lines()s.chars()s.repeat(3)onion.Strings 側ではなく JDK 標準の同名メソッド を暗黙のうちに 呼び出します。そのため s.split(",")List ではなく String[] を 返し、s.substring(10) は範囲外の開始位置で "" を返す代わりに例外を 投げ、s.lines() / s.chars()List ではなく JDK の Stream / IntStream を返し、s.repeat(-1)"" を返す代わりに例外を投げます。 これら5つのメソッドについて onion.Strings の List を返す・例外を 投げない挙動を得るには、Strings:: の静的呼び出し形式(例: Strings::split(...)Strings::substring(...))を使ってください。

Maps モジュール

Map ユーティリティ(onion.Maps)。結果 Map は挿入順を保持(LinkedHashMap)。

val m: Map[String, Int] = Maps::newMap()
Maps::getOrDefault(m, "a", 0)                 // あればその値、無ければデフォルト
Maps::getOrElse(m, "x", () -> compute())      // 遅延デフォルト
Maps::keys(m) / Maps::values(m)               // 順序を保ったリスト
m.getOrDefault("x", 0) / m.keys() / m.values() // 拡張メソッドとしても呼べる(上と同じ)
Maps::mapValues(m, (v: Int) -> v * 2) / Maps::mapKeys(m, (k: String) -> k.toUpperCase())
Maps::filterValues(m, (v: Int) -> v > 0) / Maps::filterKeys(m, (k: String) -> k.startsWith("a"))
Maps::filter(m, (k: String, v: Int) -> v > 0) // キー+値の述語
Maps::invert(m)                               // キーと値を入れ替え
Maps::toList(m, (k: String, v: Int) -> k + "=" + v)  // エントリ -> List
Maps::forEach(m, (k: String, v: Int) -> println(k))
Maps::count(m, p) / Maps::anyEntry(m, p) / Maps::allEntries(m, p)
Maps::groupBy(items, keyOf)                   // Map[K, List]
Maps::countBy(items, keyOf)                   // 頻度 Map[K, Integer]
val merged = Maps::merge(a, b)                // 衝突時は b が優先
Maps::mergeWith(a, b, (x: Int, y: Int) -> x + y)  // 衝突を結合
Maps::update(m, "a", (v: Int) -> v + 1)       // 関数的更新

Sets モジュール

Set ユーティリティ(onion.Sets)。結果 Set は挿入順を保持し、集合演算は null 安全。

どのメソッドも Set の組み込み拡張メソッドとして呼び出せます(例: Sets::union(a, b)a.union(b))。

Sets::of(1, 2, 3) / Sets::newSet[Int]() / Sets::fromList([1, 1, 2]) / Sets::toList(a)
Sets::union(a, b) / Sets::intersection(a, b) / Sets::difference(a, b)
a.union(b) / a.intersection(b) / a.difference(b)
Sets::symmetricDifference(a, b)               // どちらか一方だけに含まれる
a.symmetricDifference(b)
Sets::containsAll(a, b)                       // a が b の要素をすべて含む
a.containsAll(b)
Sets::isSubsetOf(a, b) / Sets::isSupersetOf(a, b) / Sets::isDisjoint(a, b)
a.isSubsetOf(b) / a.isSupersetOf(b) / a.isDisjoint(b)
Sets::map(a, f) / Sets::filter(a, p) / Sets::find(a, p)
a.map(f) / a.filter(p) / a.find(p)
Sets::forEach(a, (x: Int) -> println(x))
a.forEach((x: Int) -> println(x))
Sets::count(a, p) / Sets::any(a, p) / Sets::all(a, p)
a.count(p) / a.any(p) / a.all(p)

Hash モジュール

暗号学的ハッシュ・チェックサム(onion.Hash)。文字列の UTF-8 バイトをハッシュ化し、小文字 hex のダイジェストを返します。

Hash::sha256("password")   // 64文字 hex
Hash::sha512(text)         // 128文字 hex
Hash::md5(text) / Hash::sha1(text)   // チェックサム・互換用(衝突耐性なし)

いずれも String の組み込み拡張メソッドとしても使え、静的呼び出しの代わりに メソッドチェーンで書けます:

"password".sha256()        // Hash::sha256("password") と同じ
"x".base64Encode().sha256().substring(0, 8)   // 下の Codec とチェーン可能

Codec モジュール

テキストのエンコード・デコード(onion.Codec): Base64・hex・URL/パーセント形式。

val enc = Codec::base64Encode("Hello")    // "SGVsbG8="
Codec::base64Decode(enc)                  // "Hello"
Codec::hexEncode("Hi") / Codec::hexDecode("4869")
Codec::urlEncode("a b&c") / Codec::urlDecode(s)

これらも String の組み込み拡張メソッドです:

"Hello".base64Encode().base64Decode()   // "Hello"
"Hi".hexEncode() / "4869".hexDecode()
"a b&c".urlEncode() / s.urlDecode()

Stats モジュール

数値リストの集計(onion.Stats)。汎用集計は List[Int]/List[Long]/List[Double] を受け付け倍精度で計算。sumInt/sumLong は整数精度を保持。

val xs: List[Int] = [10, 20, 30, 40]
Stats::sum(xs)       // 100.0      Stats::sumInt(xs)   // 100
Stats::average(xs)   // 25.0       Stats::median(xs)   // 25.0
Stats::min(xs) / Stats::max(xs)    // 10.0 / 40.0
Stats::variance(xs) / Stats::stddev(xs)

val ys: List[Long] = [10L, 20L, 30L, 40L]
Stats::sumLong(ys)   // 100L   (Long: 整数精度を保持)

メソッド呼び出しの形でも使えます(実際のコードではこちらが自然です)。ただし メソッド形式も同じく倍精度なので、Int のリストでも合計は Double になります。 Int で受け取りたい場合は Stats::sumInt を使ってください。

val xs: List[Int] = [10, 20, 30, 40]
xs.sum()             // 100.0  (Double: 汎用集計)
Stats::sumInt(xs)    // 100    (Int)

Int を返す sum() のオーバーロードが無いのは型消去のためです。実行時には要素型が 消えるので、sum(List[Int])sum(List[Double]) は同じ JVM シグネチャになります。

Format モジュール

locale 非依存の人間可読フォーマット(onion.Format)——桁区切り・小数・サイズ・時間。

Format::integer(1234567)          // "1,234,567"
Format::number(1234.5678, 2)      // "1,234.57"
Format::fixed(3.14159, 2)         // "3.14"
Format::percent(0.756, 1)         // "75.6%"
Format::bytes(1536)               // "1.5 KB"(1024基準)
Format::duration(3661)            // "1h 1m 1s"
Format::ordinal(21)               // "21st"

いずれも数値レシーバの組み込み拡張メソッドとしても使えます(integer/bytes/ duration/ordinalLongnumber/fixed/percentDouble):

(1536L).bytes()                   // "1.5 KB"
(3661L).duration()                // "1h 1m 1s"
(21L).ordinal()                   // "21st"
(0.756).percent(1)                // "75.6%"
(3.14159).fixed(2)                // "3.14"

Text モジュール

コンソールのテキストレイアウト(onion.Text)——折返し・インデント・整列テーブル。

Text::wrap("長い文章 ...", 40)          // 折り返した行のリスト
Text::indent("a\nb", "> ")              // "> a\n> b"
Text::dedent("    a\n    b")            // "a\nb"

Text::table([["Name", "Dept"], ["Alice", "Eng"], ["Bob", "Sales"]])
// Name   Dept
// Alice  Eng
// Bob    Sales

いずれもレシーバの組み込み拡張メソッドとしても使えます(wrap/indent/dedentStringtableList):

"長い文章 ...".wrap(40)            // 折り返した行のリスト
"a\nb".indent("> ")                // "> a\n> b"
[["Name", "Dept"], ["Alice", "Eng"]].table()

System モジュール

Java の System クラスを介したシステムレベル操作へのアクセス。

System::out

標準出力ストリーム:

System::out.println("直接システム出力")
System::out.print("改行なし")

System::in

標準入力ストリーム:

import {
  java.io.BufferedReader;
  java.io.InputStreamReader;
}

val reader: BufferedReader = new BufferedReader(
  new InputStreamReader(System::in)
)

System::currentTimeMillis

現在時刻をミリ秒で取得:

val time: Long = System::currentTimeMillis()
IO::println("現在時刻: " + time)

System::getProperty

システムプロパティを取得:

val os: String = System::getProperty("os.name")
val user: String = System::getProperty("user.name")
val home: String = System::getProperty("user.home")

System::exit

プログラムを終了:

System::exit(0)  // 成功
System::exit(1)  // エラー

Iterables モジュール

onion.Iterables(Java インターフェース)で提供。

コレクションや配列向けのイテレーションユーティリティ:

  • Iterables::map(list|iterable|set, f)
  • Iterables::mapMap(map, f) - 各 Map.Entryf で変換した新しい Map を返す
  • Iterables::toList(iterable) - 任意の Iterable(範囲を含む)を List に実体化する
  • Iterables::filter(list|iterable, predicate)
  • Iterables::foldl(iterable, init, f)
  • Iterables::reduce(list, initial, reducer)
  • Iterables::exists(iterable, predicate)
  • Iterables::forAll(iterable, predicate)
  • Iterables::listOf(elements...)
  • Iterables::newList(size) - size 個分の容量を確保した空の List
  • Iterables::first(list) / Iterables::last(list) - リストが空なら null
  • Iterables::reverse(list)
  • Iterables::take(list, n) / Iterables::drop(list, n)
  • Iterables::sort(list, comparator) / Iterables::sort(list) - 後者は要素が Comparable であることが必要

上記のメソッド(listOfnewList を除く -- これらは List を操作するの ではなく生成するため)は、いずれも組み込みの拡張メソッドとしても呼び出せ、 第一引数に対するメソッドチェーンとして書ける:

xs.map { x -> x * 2 }             // Set / Iterable レシーバでも同様
m.mapMap((e) -> Colls::entry(e.getKey(), e.getValue() * 2))
(1..5).toList()                   // 範囲(Range)も対象
xs.filter { x -> x > 0 }
xs.foldl(0, (acc, x) -> acc + x)
xs.reduce(0, (acc, x) -> acc + x)
xs.exists { x -> x > 2 }
xs.forAll { x -> x > 0 }
xs.first() / xs.last()
xs.reverse()
xs.take(2) / xs.drop(1)
xs.sort() / xs.sort(comparator)

Files モジュール

ファイル I/O(onion.Files):

Files::readText("path.txt")            // ファイル全体を String として
Files::readLines("path.txt")           // List[String]
Files::writeText("out.txt", content)
Files::writeLines("out.txt", lines)    // List[String] を1行ずつ書き込む
Files::appendText("out.txt", content)  // 追記。ファイルが無ければ新規作成
Files::readBytes(path) / Files::writeBytes(path, bytes)
Files::list("dir")                     // エントリ名の List
Files::listFiles("dir")                // java.io.File エントリの List
Files::glob("dir", "*.on")             // glob にマッチしたエントリ名
Files::delete(path) / Files::exists(path)
Files::isFile(path) / Files::isDirectory(path)
Files::mkdirs(path)                    // ディレクトリと不足する親ディレクトリを作成
Files::size(path)                      // Long。バイト数(存在しなければ0)
Files::copy(src, dst)                  // dst が既にあれば置き換える
Files::move(src, dst)                  // 移動(リネーム)。dst が既にあれば置き換える
Files::copyDir(src, dst)               // ディレクトリを再帰的にコピー

パス操作ヘルパー——ファイル名・親ディレクトリ・結合・拡張子:

Files::getFileName("a/b/c.txt")        // "c.txt"
Files::getParent("a/b/c.txt")          // "a/b"
Files::getAbsolutePath("a/b/c.txt")    // カレントディレクトリ基準の絶対パス
Files::joinPath("a/b", "c.txt")        // "a/b/c.txt"
Files::ext("report.txt")               // "txt"(拡張子。予約語を避けた名前)
Files::stem("report.txt")              // "report"
Files::withExtension("report.txt", "md")   // "report.md"

Csv モジュール

RFC 4180 準拠の自己完結型 CSV パース・シリアライズ(onion.Csv、自動インポート済み)——引用フィールド・カンマ/改行を含む値・二重引用符に対応。

val rows = Csv::parse(text)                  // List of List of String
val recs = Csv::parseWithHeader(text)        // List of Map(ヘッダー -> 値)

Csv::column(rows, 0)                          // 位置指定で1列取得
Csv::columnByName(recs, "age")                // ヘッダー名で1列取得

val out  = Csv::stringify(rows)               // rows -> CSV テキスト
val out2 = Csv::stringifyWithHeader(recs)     // records -> CSV(parseWithHeader の逆)

Proc モジュール

スクリプト向けのプロセス実行(onion.Proc):

val r = Proc::capture("git", "status")  // r.status() / r.stdout() / r.stderr() / r.succeeded() / r.failed()
Proc::run("ls", "-la")                  // stdout を String で取得(失敗時は例外)
Proc::exec("make", "build")             // 終了コード、出力はそのまま素通し
Proc::captureIn("/tmp", "ls")           // ...In 系は作業ディレクトリを指定
Proc::runIn("/tmp", "ls")               // run と同様だが、指定した作業ディレクトリで実行
Proc::execIn("/tmp", "make", "build")   // exec と同様だが、指定した作業ディレクトリで実行

Args モジュール

コマンドライン引数のパース(onion.Args):

val parsed = Args::parse(args)
parsed.flag("verbose")                  // --verbose
parsed.option("out", "a.out")           // --out path(デフォルト値付き)
parsed.intOption("level", 3)
parsed.positional()                     // オプション以外の引数の List

Colls モジュール

コレクションのファクトリとパイプライン(onion.Colls):

Colls::listOf("a", "b", "c")            // 不変の List
Colls::mutableListOf(1, 2, 3)           // ArrayList
Colls::range(0, 5)                      // List [0,1,2,3,4]
Colls::rangeWithStep(0, 10, 2)          // List [0,2,4,6,8]
Colls::sortedBy(people) { p -> p.age() }
// map/filter/reduce/fold のパイプラインは List/Iterable/配列の拡張メソッド:
// xs.map { x -> x * 2 }.filter { x -> x > 0 }

追加のファクトリ: Set・Map・空のコレクション

Colls::setOf("a", "b", "c")             // 不変の Set(反復順序は保証されない)
Colls::mutableSetOf(1, 2, 3)            // HashSet

Colls::entry("name", "Alice")           // mapOf/mutableMapOf 用の Map.Entry
Colls::mapOf(Colls::entry("name", "Alice"), Colls::entry("age", "30"))   // 不変の Map、挿入順を保持
Colls::mutableMapOf(Colls::entry("x", 1))                               // HashMap

Colls::emptyList()                      // []
Colls::emptySet()                       // 空の Set
Colls::emptyMap()                       // 空の Map

List・Set・Map のユーティリティ

Colls の他のメソッドと同様、最初の引数(list/map)に対する拡張メソッドとしても 呼び出せ、パイプラインとして連結できる:

xs.concat(ys)                     // xs の要素に続けて ys の要素
[[1, 2], [3, 4]].flatten()        // [1, 2, 3, 4] - ネストを1段階解消
xs.flatMap { x -> [x, x] }        // 各要素をリストに写してから1段階平坦化する
                                   // (bind はその別名で、do[List] { x <- xs; ... } が使う)
xs.partition { x -> x > 1 }       // [matching, nonMatching] - 2つの List
xs.toSet()                        // xs の要素から作った Set
xs.distinct()                     // 重複を除去、最初に出現した順を保持
xs.slice(0, 2)                    // [0, 2) の部分リスト、範囲内にクランプ
xs.sorted()                       // 昇順の新しい List(要素は Comparable である必要がある)
xs.sortedByDescending { x -> x }  // sortedBy と同様だが降順
xs.head()                         // 先頭要素、空ならnull(first の別名)
xs.tail()                         // 先頭要素を除いた残り(空リストでは例外)
xs.takeWhile { x -> x < 3 }       // 述語を満たす先頭の連続部分
xs.dropWhile { x -> x < 3 }       // その先頭の連続部分を取り除いた残り
xs.zip(ys)                        // [[x0, y0], [x1, y1], ...] - ペアのList、短い方に合わせて切り詰め
xs.groupBy { x -> x % 2 }         // キーごとの要素の List を値に持つ Map
xs.mkString(", ")                 // "1, 2, 3" - 要素を文字列として連結(join は別名)
Colls::isNotEmpty(xs)             // true - isEmpty の否定
m.filterMap { k, v -> k == "name" }   // 条件に合うエントリだけの Map
xs.any { x -> x > 1 }             // いずれかの要素が条件を満たせば true
xs.all { x -> x > 0 }             // すべての要素が条件を満たせば true
xs.none { x -> x > 5 }            // どの要素も条件を満たさなければ true
xs.find { x -> x > 1 }            // 最初に条件を満たす要素、なければ null
xs.forEach { x -> println(x) }    // 各要素に対して処理を実行、戻り値なし
xs.count { x -> x > 1 }           // 条件を満たす要素の数
xs.reverse()                      // 要素を逆順にした新しい List
xs.contains(2)                    // いずれかの要素が2と等しければ true
Colls::toList(args)               // Java配列(例: main の String[])を List に変換

バッチ化・ウィンドウ化・セレクタ集計

これらも Colls:: の静的呼び出しとして、また Colls の他のメソッドと同様に パイプラインとして連結できる List の拡張メソッドとして利用できる:

xs.chunked(3)                     // [[1,2,3],[4,5,6],[7]] - 最大3件のバッチ、最後は少なくなることがある
xs.windowed(3)                    // [[1,2,3],[2,3,4],[3,4,5]] - 1要素ずつスライドする窓
ps.sumBy((p) -> p.age())          // Double - 各要素にセレクタを適用した合計
ps.averageBy((p) -> p.age())      // Double - セレクタの平均、空なら0.0
ps.maxBy((p) -> p.age())          // セレクタの値が最大の要素、空ならnull
ps.minBy((p) -> p.age())          // セレクタの値が最小の要素、空ならnull

xs.chunked(2).map { b -> (b as List).size() }   // 他のパイプライン段と同様に連結できる

Http

HTTPクライアントユーティリティ(Java 11+ の HttpClient を使用)。

GET リクエスト

Http::get(url): String
Http::get(url, headers): String    // headers: ["Name1", "Value1", ...]

POST リクエスト

Http::post(url, body): String
Http::postJson(url, jsonBody): String    // Content-Type: application/json を設定
Http::post(url, body, headers): String   // headers は get と同じ

Response オブジェクト

Http::getResponse(url): Response                  // ボディだけでなく status/body/headers を返す
Http::postResponse(url, body): Response

Responsestatus: Intbody: Stringheaders: List のフィールドと、 isOk(): Boolean(2xx)・isError(): Boolean(4xx/5xx)のヘルパーを持つ。 ボディだけでなくステータスコードやヘッダーが必要なときに使う。

その他のメソッド

Http::put(url, body): String
Http::delete(url): String

URL ユーティリティ

Http::encodeUrl(str): String
Http::decodeUrl(str): String
Http::buildQuery(params): String        // params: キーと値を交互に並べる
Http::buildUrl(baseUrl, params): String // "?"/"&" + buildQuery(params) を付加

val response: String = Http::get("https://api.example.com/data");
val data: Object = Json::parse(response);

val postResponse: String = Http::postJson(
  "https://api.example.com/users",
  "{\"name\": \"Bob\"}"
);

DateTime

エポックミリ秒を使った日時ユーティリティ。

現在時刻

DateTime::now(): Long              // 現在のエポックミリ秒
DateTime::nowString(): String      // ISO 形式(ローカルタイムゾーン)
DateTime::nowString(pattern): String

パース

DateTime::parse(isoString): Long
DateTime::parse(dateTime, pattern): Long

フォーマット

DateTime::format(epochMillis): String
DateTime::format(epochMillis, pattern): String

構成要素

DateTime::year(epochMillis): Int
DateTime::month(epochMillis): Int       // 1-12
DateTime::day(epochMillis): Int         // 1-31
DateTime::hour(epochMillis): Int        // 0-23
DateTime::minute(epochMillis): Int      // 0-59
DateTime::second(epochMillis): Int      // 0-59
DateTime::dayOfWeek(epochMillis): Int   // 1=月曜, 7=日曜
DateTime::dayOfYear(epochMillis): Int   // 1-366

演算

DateTime::addDays(epochMillis, days): Long
DateTime::addHours(epochMillis, hours): Long
DateTime::addMinutes(epochMillis, minutes): Long
DateTime::addSeconds(epochMillis, seconds): Long
DateTime::addMonths(epochMillis, months): Long
DateTime::addYears(epochMillis, years): Long

比較

DateTime::diff(time1, time2): Long        // ミリ秒単位の差分
DateTime::diffDays(time1, time2): Int
DateTime::diffHours(time1, time2): Long   // hours / minutes / seconds は整数値
DateTime::diffMinutes(time1, time2): Long
DateTime::diffSeconds(time1, time2): Long
DateTime::isBefore(time1, time2): Boolean
DateTime::isAfter(time1, time2): Boolean
DateTime::dayName(epochMillis): String    // "Friday"(英語、ロケール非依存)
DateTime::monthName(epochMillis): String  // "March"

ファクトリ

DateTime::of(year, month, day): Long
DateTime::of(year, month, day, hour, minute, second): Long
DateTime::startOfDay(epochMillis): Long
DateTime::endOfDay(epochMillis): Long

val now: Long = DateTime::now();
IO::println("今日: " + DateTime::format(now, "yyyy-MM-dd"));

val tomorrow: Long = DateTime::addDays(now, 1);
IO::println("明日: " + DateTime::format(tomorrow));

val birthday: Long = DateTime::of(1990, 5, 15);
val age: Int = DateTime::diffDays(now, birthday) / 365;

Net

TCP ソケット。Http がリクエストを送る側なのに対し、こちらは任意のプロトコルを話し、接続を受けられます。

Net::connect

val conn = Net::connect("example.com", 80)
conn.writeLine("GET / HTTP/1.0")
conn.writeLine("Host: example.com")
conn.writeLine("")
IO::println(conn.readAll())
conn.close()

Net::connect(host, port, timeoutMillis) はタイムアウトを指定できます。OS 既定のままだと、 パケットが落ちた場合に 1 分以上待つことがあります。

接続は readLine()(終端で null)、readAll()(UTF-8、相手が閉じるまで)、readBytes() で読み、 write(text)writeLine(text)(CRLF を付加。行指向プロトコルが期待する形)、writeBytes(bytes) で書きます。書き込みは毎回フラッシュするので、バッファに溜まったまま送られないことはありません。 timeout(millis) はブロックする読み込みの上限、closeWrite() は読みを続けたまま書き側だけ閉じて EOF を通知します。close() は冪等で、conn.isClosed() で既に閉じているかを確認できます。

Net::listen

val listener = Net::listen("localhost", 0, 4)   // 0 で OS に空きポートを選ばせる
IO::println("listening on " + listener.port())

val peer = listener.accept()
peer.writeLine("hello " + peer.remoteAddress())
peer.close()
listener.close()

ポート 0 は OS に空きポートを選ばせ、port() が実際のポートを返します。番号を決め打ちして祈らずに サーバをテストできるのはこのためです。"localhost" を指定するとネットワークからは到達できません。 ホストに null を渡すとすべてのローカルアドレスにバインドします。accept() でブロックしている スレッドを解除するにはリスナーを閉じます。閉じたかどうかは listener.isClosed() で分かります。

Net::listen(port) は最後のケースの省略形で、バックログのデフォルト値 50 ですべてのローカル アドレスにバインドします。Net::listen(null, port, 50) と同等です。

失敗時は失敗したアドレスがメッセージに入るので、Onion の catch で「connection refused」だけでなく どのホストかが分かります。


Server

HTTP サーバ。JDK 同梱の実装を使うので依存は増えません。

Server::start

val server = Server::start("localhost", 8080)
server.handle("/hello", (req) -> Server::text("hi " + req.method()))
server.await()

Server::start(port) はすべてのローカルアドレス、Server::start(host, port) は 1 つだけに バインドします。ポート 0 なら OS が空きポートを選び、port() が返します。await() はプロセスが 終わるまでブロックし、stop() は受付を止めて処理中のリクエストを 1 秒待ちます。

ルーティング

handle(path, handler) は完全一致、handleAll(handler) はそれ以外すべてを受けます。Onion 側で ルーティングを書くならこちらです。

server.handleAll((req) -> select req.path() {
  case re"/users/(\d+)" (id): Server::json("{\"id\":" + id + "}")
  case "/health":             Server::text("ok")
  else:                       Server::notFound()
})

ハンドラが例外を投げた場合は 500 を返します。サーバが落ちたり、クライアントが応答のないソケットで 待ち続けたりすることはありません。

Request

method()path()(クエリ文字列を含まない)、query()(生のクエリ文字列。無ければ "")、 body()(ハンドラ実行前に全部読み込み済み)、header(name)headers()params()。 最後の 2 つは記述順を保った Map を返します。

Response

Server::textServer::jsonServer::html は対応する Content-Type 付きの 200、 Server::notFound() は 404、Server::status(code, body) はそれ以外です。レスポンスは不変なので、 withStatuswithHeader は新しい値を返します。

val r = Server::json("{\"a\":1}").withStatus(201).withHeader("X-Test", "yes")

レスポンスの構築はソケットに一切触れません。ハンドラを単体でテストできるのはこのためです。


Archive

zip と gzip。tar は依存が必要になるため入れていません。この 2 つが JDK 単体でできる範囲です。

Archive::zip("out.zip", ["a.txt", "b.txt"])
Archive::zipDir("site.zip", "site")          // "site" からの相対パスを保つ
val names = Archive::entries("out.zip")      // 展開せずに一覧
val written = Archive::unzip("out.zip", "extracted")

Archive::gzipFile("big.log", "big.log.gz")   // 全部をメモリに読まずストリーミング
Archive::gunzipFile("big.log.gz", "big.log") // その逆
val bytes = Archive::gunzip(Archive::gzip(text.getBytes()))

展開は、対象ディレクトリの外に書き出すことを拒否します。 ../../.ssh/authorized_keys という 名前のエントリは「zip slip」と呼ばれる古典的な攻撃で、エントリ名を素直に解決する展開処理は 言われたとおりの場所に書き込んでしまいます。ここではエントリ名を示して例外を投げます。

エントリのタイムスタンプは固定値で書き込むので、同じ入力を 2 回 zip すると同じバイト列になります。 実行のたびに変わる成果物はチェックサムもキャッシュもできません。


Concurrent

スレッドと、それを安全に使うための部品。Future は既に「1 つの処理を別スレッドで走らせる」ことは できましたが、同時実行数を制限する手段、スレッド間でカウンタを共有する手段、ロックを取る手段、 処理を受け渡す手段がありませんでした。

仮想スレッドは意図的に入れていません。Java 21 が必要で、Onion のターゲットは 17 です。

Pool

val pool = Concurrent::pool(4)                     // Concurrent::pool() で CPU 数
val bodies = pool.mapAll(urls, (u) -> Http::get(u))
pool.close()

mapAll は結果を入力の順序で返します。完了順ではありません——タイミングに依存する出力は テストできない出力です。失敗した要素は全タスクが決着してから報告されるので、1 つの不正な入力の せいで、諦めた呼び出し元の裏でワーカーが走り続けることはありません。単発の処理には submit(f)Future を返します。

プールのスレッドはデーモンなので、閉じ忘れたプールが main の後も JVM を生かし続けることは ありません。とはいえ close() は呼ぶべきですし、処理中のものを待つなら awaitClose(millis) です。

API 一覧:

  • Concurrent::cpus() - このマシンで使えるプロセッサ数(引数なしの Concurrent::pool() が 採用するサイズ)
  • pool.size() - このプールのスレッド数
  • pool.submit(f) - f をワーカーで実行し、Future を返す
  • pool.mapAll(items, f) - 上記の通り
  • pool.close() - 新規の受け付けを止め、実行中のものを中断する。冪等
  • pool.awaitClose(timeoutMillis) - 新規の受け付けを止め、実行中のものを待つ。 タイムアウト内に全て終わったかを返す

Counter・Lock・Channel

val hits = Concurrent::counter()
hits.increment()

val lock = Concurrent::lock()
lock.withLock(() -> { /* … */ })     // 本体が例外を投げても解放される

val chan = Concurrent::channel(16)   // 意図的に上限つき
chan.send("work")
val item = chan.receiveTimeout(1000) // 永久にブロックせず null を返す

acquire/release の手動ペアより withLock を使ってください。ペアの間で例外が投げられると ロックが漏れ、他のスレッドが永久に待ちます。チャネルに上限があるのは、無制限だと生産者が 消費者を追い越していることがメモリ枯渇まで見えないからです。null の送信は拒否します—— 受信側で「何も来なかった」と区別できなくなるためです。

API 一覧:

  • Concurrent::counter() / Concurrent::counter(initial)
  • counter.get() / counter.increment() / counter.decrement() / counter.add(delta) / counter.set(next)
  • counter.compareAndSet(expected, next) - 値がまだ expected と等しい場合のみ設定する
  • Concurrent::lock()
  • lock.withLock(body) - 上記の通り
  • lock.acquire() / lock.release() - withLock が避けるための手動ペア
  • lock.tryAcquire() - ロックが空いているときだけ取得する。取得できたかを返す
  • lock.isHeld() - ロックが現在保持されているか
  • Concurrent::channel(capacity)
  • chan.send(item) / chan.trySend(item) - 満杯ならブロック / 満杯なら false を返す
  • chan.receive() / chan.receiveTimeout(timeoutMillis) - 何か届くまでブロック / タイムアウト後は null を返す
  • chan.size() / chan.isEmpty()
  • chan.close() / chan.isClosed() - 以降の送信を拒否する。既にキューにあるものは受信できる
  • chan.drain() - 現在キューにあるもの全てを取り出し、チャネルを空にする

Db

JDBC 経由の SQL。ドライバは同梱していません。プロジェクトが宣言したものを使います。

[dependencies]
"org.postgresql:postgresql" = "42.7.3"
val db = Db::connect("jdbc:postgresql://localhost/app", "user", "secret")

val rows = db.query("SELECT id, name FROM users WHERE age > ?", 18)
val one  = db.queryOne("SELECT * FROM users WHERE id = ?", 7)   // 該当なしなら null
val n    = db.queryValue("SELECT COUNT(*) FROM users")          // 先頭行の先頭列
db.update("INSERT INTO users VALUES (?, ?)", 8, "ada")

db.transaction((conn) -> {
  conn.update("UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1)
  conn.update("UPDATE accounts SET balance = balance + ? WHERE id = ?", 100, 2)
})

db.isClosed()   // db.close() を呼ぶまでは false
db.close()

Db::connect(url) には認証情報が不要なデータベース(SQLite、H2 など)向けの引数 1 つの形式 もあります。Db::connect("jdbc:sqlite:local.db")Db::connect(url, null, null) と同じです。

値は常にバインドされ、SQL 文字列に埋め込まれることはありません。WHERE name = ? は どんな名前でも安全で、うっかり文字列連結してしまう余地がそもそもありません。

トランザクションは本体が正常終了すればコミット、例外を投げればロールバックし、そのうえで 再スローします。begin/commit のペアを忘れる余地はありません。手動のペアの間で例外が投げられると 接続はトランザクションを開いたままになり、次の無関係な文がそれに巻き込まれます。

行は「列ラベル → 値」の Map で、選択した順序を保ちます。列名ではなくラベルなので SELECT x AS yy になります。同じラベルの列が 2 つある場合は、片方を黙って失う代わりに 拒否します。AS で別名を付けてください。


Regex

正規表現ユーティリティ。

マッチング

Regex::matches(input, pattern): Boolean   // 文字列全体がマッチ
Regex::find(input, pattern): Boolean      // どこかにパターンが見つかる

抽出

Regex::findAll(input, pattern): List[String]
Regex::findFirst(input, pattern): String
Regex::groups(input, pattern): List[String]   // 最初のマッチのグループ
Regex::groupsAll(input, pattern): List[List[String]]  // 全マッチのグループ

置換

Regex::replace(input, pattern, replacement): String
Regex::replaceFirst(input, pattern, replacement): String

分割

Regex::split(input, pattern): List[String]
Regex::split(input, pattern, limit): List[String]

ユーティリティ

Regex::quote(literal): String    // 特殊文字をエスケープ
Regex::isValid(pattern): Boolean

アンカー付きマッチ

Regex::matchGroups(input, pattern): List[String]

input 全体pattern にマッチしたときだけマッチしたとみなし(find/findAll のようにどこかにマッチすれば良いのではなくアンカー付き)、マッチしなければ null を 返す。マッチした場合はキャプチャグループを返す(インデックス0がグループ1)。マッチに 参加しなかったグループは null ではなく "" になる。これは case re"..." (a, b): という select パターン(CLAUDE.md の「Regex literals」を参照)を支える基本操作で、 コンパイラはアンカー付き正規表現パターンを matchGroups 呼び出しと null チェックに 脱糖する。

Pattern リテラルのオーバーロード

re"..." リテラルは String ではなく java.util.regex.Pattern にコンパイルされます。 上記のマッチング/抽出/置換/分割の各メソッドには、コンパイル済み Pattern を直接 受け取るオーバーロードも用意されており、re"..." リテラルを String パターンを 経由せずそのまま渡せます:

Regex::matches(input, pattern: Pattern): Boolean
Regex::find(input, pattern: Pattern): Boolean
Regex::findAll(input, pattern: Pattern): List[String]
Regex::findFirst(input, pattern: Pattern): String
Regex::groups(input, pattern: Pattern): List[String]
Regex::groupsAll(input, pattern: Pattern): List[List[String]]
Regex::replace(input, pattern: Pattern, replacement): String
Regex::replaceFirst(input, pattern: Pattern, replacement): String
Regex::split(input, pattern: Pattern): List[String]
Regex::split(input, pattern: Pattern, limit): List[String]
val p = re"[\w.]+@[\w.]+";
val emails: List[String] = Regex::findAll("alice@example.com", p);

val text: String = "Email: alice@example.com, bob@test.org";
val emails: List[String] = Regex::findAll(text, "[\\w.]+@[\\w.]+");
// ["alice@example.com", "bob@test.org"]

val masked: String = Regex::replace(text, "@[\\w.]+", "@***");
// "Email: alice@***, bob@***"

if (Regex::matches("hello123", "[a-z]+\\d+")) {
  IO::println("パターンマッチ!");
}

次のステップ