AGENT SKILL

run2_scala-builder

Implementing an immutable fluent builder in Scala with companion object apply, varargs metadata, and a build() method returning a function — translating Python's mutable builder class.

License 许可
MIT
最近更新
2026-10-02
Stars
★ 93
Forks
8

01 前言 Front Matter

name
run2_scala-builder
description
Implementing an immutable fluent builder in Scala with companion object apply, varargs metadata, and a build() method returning a function — translating Python's mutable builder class.

03 SKILL.md 正文

Scala Immutable Fluent Builder Pattern

Python Mutable Builder → Scala Immutable Builder

Python accumulates state by mutating lists/dicts:

class TokenizerBuilder(Generic[T]):
    def __init__(self):
        self._normalizers: list[Callable[[str], str]] = []
        self._validators: list[Callable[[T], bool]] = []
        self._metadata: dict[str, Any] = {}

    def with_normalizer(self, norm) -> "TokenizerBuilder[T]":
        self._normalizers.append(norm)  # mutates
        return self

    def build(self) -> Callable[[T], Token]:
        normalizers = self._normalizers.copy()  # defensive copy needed!
        ...

Scala uses an immutable builder — each with* call returns a new instance:

final class TokenizerBuilder[T] private (
  normalizers: List[String => String],
  validators:  List[T => Boolean],
  meta:        Map[String, Any]
) {
  def withNormalizer(f: String => String): TokenizerBuilder[T] =
    new TokenizerBuilder(normalizers :+ f, validators, meta)    // creates new instance

  def withValidator(p: T => Boolean): TokenizerBuilder[T] =
    new TokenizerBuilder(normalizers, validators :+ p, meta)

  def withMetadata(entries: (String, Any)*): TokenizerBuilder[T] =
    new TokenizerBuilder(normalizers, validators, meta ++ entries.toMap)

  def build(): T => Token = { value =>
    validators.foreach { v =>
      if (!v(value)) throw new IllegalArgumentException(s"Validation failed for $value")
    }
    val str = normalizers.foldLeft(value.toString)((s, norm) => norm(s))
    Token(str, TokenType.STRING, meta)
  }
}

Benefits of immutable builder:

  • No defensive copy needed in build() — captured state is already immutable
  • Thread-safe by default — no shared mutable state
  • Each intermediate builder is a valid value (can branch/share)

Companion Object for Clean API

Python: builder = TokenizerBuilder() Scala companion:

object TokenizerBuilder {
  def apply[T](): TokenizerBuilder[T] =
    new TokenizerBuilder[T](Nil, Nil, Map.empty)
}

Usage (calling convention matches Python):

val tokenizer = TokenizerBuilder[String]()   // apply() invoked implicitly
  .withNormalizer(_.toLowerCase)
  .withNormalizer(_.replace(" ", "_"))
  .withValidator(_.nonEmpty)
  .withMetadata("type" -> "custom")          // varargs tuple syntax
  .build()

val token = tokenizer("Hello World")
// token.value    == "hello_world"
// token.metadata == Map("type" -> "custom")

Key Design Choices

Python Scala Reason
Mutable list.append list :+ elem → new list Immutability, thread safety
**kwargs in withMetadata (String, Any)* varargs Scala convention for map-like params
dict.update map ++ other.toMap Immutable map merge
Callable[[T], Token] T => Token (function type) Scala first-class function type
for v in validators foldLeft for normalizers Functional pipeline

WhitespaceTokenizer — Practical Tokenizer with Options

final class WhitespaceTokenizer(
  lowercase:        Boolean     = false,
  minLength:        Int         = 0,
  maxLength:        Option[Int] = None,
  stripPunctuation: Boolean     = false
) {
  private val punctuation: Set[Char] =
    Set('.', ',', '!', '?', ';', ':', '\'', '"', '(', ')', '[', ']', '{', '}')

  // Python's str.strip(chars) equivalent: drop from both ends
  private def processWord(word: String): Option[String] = {
    val stripped =
      if (stripPunctuation)
        word.dropWhile(punctuation).reverse.dropWhile(punctuation).reverse
      else word
    val lowered = if (lowercase) stripped.toLowerCase else stripped
    if (lowered.length < minLength) None
    else {
      val truncated = maxLength.filter(_ < lowered.length).fold(lowered)(lowered.take)
      if (truncated.isEmpty) None else Some(truncated)
    }
  }

  def tokenize(text: String): List[Token] = {
    val words = if (text.trim.isEmpty) Array.empty[String] else text.trim.split("\\s+")
    words.zipWithIndex.toList.flatMap { case (word, idx) =>
      processWord(word).map(Token(_, TokenType.STRING, Map("position" -> idx, "original" -> word)))
    }
  }
}

Python str.strip(chars) → Scala pattern:

word.dropWhile(charSet).reverse.dropWhile(charSet).reverse

This correctly removes any characters from charSet at both the start and end of the string.

04 同仓库更多 Skills