All projects coordinated by the Scala Center:

Announcement: https://www.scala-lang.org/blog/2026/01/27/sta-invests-in-scala.html
Team for the standard library project:

In order of appearence: Jamie Thompson, Nguyen Pham, Bill Venners, Chee Seng Chua, Seth Tisue, Solal Pirelli, and Hamza Remmal
🔄 Iterate for continuous discovery. Interview notes
| Proposal | Author | PR state | PR |
|---|---|---|---|
into modifier for Factory / BuildFrom | @halotukozak | MERGED | #26326 |
<, <=, >, >= compiletime ops for String | @wtetzner | MERGED | #24748 |
Seq(1,2,3) backed by Vector | @lihaoyi | DRAFT | #26386 |
Buffer(1,2,3) backed by ArrayDeque& merge Deque API with Buffer. | @lihaoyi | DRAFT | #25034 |
.andThen for multi-argument functions | @hepin1989 | DRAFT | #26077 |
Tuple(1,2,3...,99) constructor/pattern | @rssh | DRAFT | #24874 |
Replace *Ops wrappers with extensions | @halotukozak | OPEN | #26637 |
Derive ToExpr / FromExpr | @halotukozak | OPEN | #26541 |
More SizeCompareOps overloads | @marconilanna | DECLINED | #25524 |
Shared Function.identity | @hepin1989 | DECLINED | #26076 |
Plugin phases in typeCheckErrors | @soronpo | ↩️ Withdrawn | #26538 |
scala/scala3 implementations · Search SLC discussions
| Proposal | Proposed by |
|---|---|
math.clamp | @exoego |
Map.unionWith | @jackkoenig |
String.splitToIArray | @BalmungSan |
Iterable.frequencies | @spamegg1 |
mutable.Map.merge | @erikvanoosten |
Iterable.takeTo/dropTo | @dcsobral |
Iterable.runs | @exoego |
Iterable.lazyZipAll | @neontorrent |
Iterable.zipStrict | @jackkoenig |
Drafts in staging · Jamie Thompson (implementation) · Chee Seng Chua (refinement)
| Proposal | Proposed by |
|---|---|
Iterable.mapWithIndex | @BalmungSan |
Iterable.mapAccumulate | @jackkoenig |
Iterable.groupFlatMap | @ashleymercer |
Map.getAndRemove | @benhutchison |
Map.invert | @soronpo |
Set.fullIntersection | @Lasering |
SeqOps.deleted/updatedWith/splitAround | @charpov |
Iterable.groupByOrdered | @soronpo |
SeqSet trait | @NthPortal, @SethTisue |
Drafts in staging · Jamie Thompson (implementation) · Chee Seng Chua (refinement)
dotty-staging/dotty:scala-reflect-port
wick library wraps Spark with typesafe datasets.
Unfortunately broken on Scala 3.8+ due to the unified Scala library.
Spark loads scala-reflect that looked for Scala 2 signatures.
PROTOTYPE
scala-reflect ported to Scala 3
100% test coverage, drop-in, enables Wick to load.
should we release this? 🤠
Eliminate Range.foreach · DRAFT branch
dotty-staging:inline-range-foreach-miniphase
for i <- 0 to 10 do println(i)
rewritten at compiletime to:
val res = foo()
println(res)
res
rewritten at compiletime to:
var i = 0
while i <= 10 do
println(i)
i += 1
Goal: keep thread busy instead of blocked on I/O.
User space
Kernel space
Watch read readiness
Blocking wait · collect events
Ready events · one batch
read₁ · 1 syscall
read₂ · 1 syscall
read₃ · 1 syscall
Process results
+ other work
Watch → wait → N read syscalls.
Socket reads · Kqueue · epoll · io_uring · Kqueue demo how to handle both models for Gears?
One thread per reactor · queue compute tasks, pending I/O, and timers
Event queue
Dispatch on the reactor thread
Task A · Task B
Task A calls perform(read) · A suspends
Task B
Run other work · A's read is still pending
I/O finishes → completion enters the event queue
Completion A
Handle the outcome · schedule A to resume
Task A · resumed
Process the result · continue after perform
Kqueue: readiness → perform I/O → completion
io_uring: submit I/O → completion · design target
Reactor interface · portable backend set still in progress
RPC: write request to server, wait for response
multiplatform API · (JVM, Native, Wasm)
ReactorFutures.run { r =>
while buf.hasRemaining() do
perform(r,
r.ops.write(socket, buf))
var running = true
while running do
buf.clear()
perform(r, r.ops.read(socket, buf))
running = buf.position() != 0
if running then
buf.flip()
drainTo(buf, response)
}
run creates an event queue "reactor" r on the current thread, scheduling its argument.
Stage 1: loop to write to socket
perform registers the command to the reactor, and suspends the task. Reactor resumes it post-completion.
write creates a command, upon resume I/O was already performed.
Appears like blocking code: concurrency is abstracted.
Reactor + Gears socket rewrite · Kqueue demo · setup and error handling omitted
RPC: write request to server, wait for response
multiplatform API · (JVM, Native, Wasm)
ReactorFutures.run { r =>
while buf.hasRemaining() do
perform(r,
r.ops.write(socket, buf))
var running = true
while running do
buf.clear()
perform(r, r.ops.read(socket, buf))
running = buf.position() != 0
if running then
buf.flip()
drainTo(buf, response)
}
Stage 2: loop to read from socket
Keep performing read until nothing is written to buf.
drain after each read to the response.
Appears like blocking code: concurrency is abstracted.
Reactor + Gears socket rewrite · Kqueue demo · setup and error handling omitted
def onWorkerPool(
body: () ->{any.except[ThreadLocal]} Unit
): Unit =
workerPool.execute(() => body())
ReactorFutures.run { r => // reactor thread
onWorkerPool(() =>
perform(r, r.ops.read(socket, buf))
)
Async.group:
val pending = Future:
perform(r, r.ops.read(socket, buf))
buf.clear() // rejected: until group ends
pending.await
buf.clear() // allowed: group ended
}
onWorkerPool runs callback on a separate thread pool, rejecting thread-local state.
Error: body captures ThreadLocal reactor r.
(onWorkerPool is not the reactor thread.)
Error: buf is unavailable to the parent until the group ends, even after pending.await.
Group exit restores the parent's access to buf.
Classifying Capabilities · illustrative I/O contracts and diagnostics
Future Vision for language safety: statically enforce where tasks run and what resources they may retain.
Incubator for the Result[T, E] type.
Result · success or a typed error
enum Result[+T, +E]:
case Ok[+T](value: T)
extends Result[T, Nothing]
case Err[+E](error: E)
extends Result[Nothing, E]
compose dependent values
def parseInt(s: String): Result[Int, Err] = …
val (a, b) = ("123", "456")
Result:
parseInt(a).ok + parseInt(b).ok
.ok unwraps or breaks out of Result: with Err.
SIP-84 Under review evolves this to an unboxed representation
Result[T, E] is now T ? E
Result: is now maybe:
r.ok is now r?
Stage 1 · all fields present, now parse and check
Name
Ada
✓ Name is present
Age
sixteen
✕ Enter a whole number
ada@
✕ Enter a valid email
Stage 2 · checks that need a valid field
↓ No parsed age
18 or older?
Skipped until parsing succeeds
Collect errors across fields. Stop only the checks that depend on a failed value.
Validation prototype · the shared API and its types are still open.
Account form · all required fields must be present
import steps.result.Validation
import Validation.{scope, Validated}
case class Form(
name: String, age: String, email: String)
def checkFields(f: Form): Validated[Unit, String] =
Validation.validated:
scope.test(f.name.nonEmpty, "Name: required")
scope.test(f.age.nonEmpty, "Age: required")
scope.test(f.email.nonEmpty, "Email: required")
Validated[A, E] =:=
Result[A, List[E]]
Open a validation scope.
scope value accumulates errors.
Simple Boolean Tests
no early abort, failures are accumulated.
checkFields(Form("", "", "")) → three required-field errors, together.
case class Account(name: String, age: Int, email: Email)
def validateForm(f: Form): Validated[Account, String] =
Validation.validated:
// stage 0
scope.testAll(checkFields(f)).ok
// stage 1
val n = scope.test(parseName(f.name))
val a = scope.test(parseInt(f.age))
val e = scope.test(parseEmail(f.email))
// stage 2
val age = a.ok
scope.require(age >= 18, "Age: must be 18+")
Account(n.ok, age, e.ok)
"sixteen" → parse error · "16" → age rule error · "21" → success
Stage 0: merge errors: add to scope
Stage 0: .ok breaks early on stage 0 error.
State 1: Parse values with Result
each test may record an error in the scope, returning a handle on the value.
Stage 2: extract age
.ok breaks early if a had an error.
Stage 2: Validate age
Stage 2: break early with require
Stage 2: Extract remaining Result
don't force before checking age.
Batch signup · flag a duplicate email on both forms
def checkEmailConflict(a: Form, b: Form)(
aErrors: Validation[String]^,
bErrors: Validation[String]^
): Unit =
val dupMsg = "Email: duplicated in batch"
val distinct = a.email != b.email
aErrors.test(distinct, dupMsg)
bErrors.test(distinct, dupMsg)
Rejected · pass the same collector twice
Validation.validated[Unit, String] {
checkEmailConflict(a, b)(scope, scope)
}
Validation is stateful and exclusive
class Validation[E]
extends Stateful,
ExclusiveCapability:
// implementation omitted
^ grants exclusive write capability.
One writer for each form's error list.
Error: Separation Failure
Arguments aErrors and bErrors both capture scope.
A duplicate email gives each form its own error. Two write parameters cannot alias.
Before
@main def serve(
host: String, port: Int = 8080, files: String*
): Unit = ...
After
@cmd def serve(
host: String, port: Int = 8080, files: String*
): Unit = ...
@main def run(args: String*): Unit =
Flagged.parseOrExit[this.type](args)
./serve.scala localhost 7777 foo.scala
unchanged except for @main → @cmd.
foo.type => search for @cmd methods.
default behavior same as @main:
positional parameters
Before
@cmd def serve(
host: String, port: Int = 8080, files: String*
): Unit = ...
After
case class Serve(
host: String, port: Int = 8080, files: String*
) derives Parser.Command
@main def run(args: String*): Unit =
val serve = Flagged.parseOrExit[Serve](args)
...
./serve.scala localhost 7777 foo.scala
typical type-class derivation flow.
class type => look for Parser[T].
still same default behavior
Previously · positional arguments
./serve.scala localhost 7777 foo.scala
Add annotations to the same method
case class Serve(
@opt host: String,
@opt(short = 'p') port: Int = 8080,
files: String*
) derives Parser.Command
Now · named options, with the same values
./serve.scala -p 7777 --host localhost foo.scala
./serve.scala --host localhost foo.scala
@opt makes a parameter named.
port accepts --port or -p.
Omit --port to use default arg.
Unannotated files stays positional.
Named parameters in any order!
Same method body and Flagged.parseOrExit[this.type](args) entry point
Git-style CLI · two levels of subcommands
enum Gitto derives Parser.CommandGroup:
case Clone(repo: String,
@opt(short = 'd') depth: Option[Int] = None)
case Remote(action: RemoteAction)
case Status(@opt(short = 's') short: Boolean = false)
enum RemoteAction derives Parser.CommandGroup:
case Add(name: String, url: String)
case Remove(name: String)
@main def gitto(args: String*): Unit =
val cfg = Flagged.parseOrExit[Gitto](args)
...
./gitto.scala remote add origin \
https://github.com/lampepfl/gears.git
uses type-class derivation
Level 1: remote selects a nested command group.
Level 2: add selects a command within that group; name and url are positional arguments.
One parse returns a nested value:
Gitto.Remote(
RemoteAction.Add(...))
Gitto example · --help at each command level
Annotations · option names, flags, help
trait AnnotMirror[T]:
type MirroredSelfAnnotations <: Tuple
type MirroredAnnotations <: Tuple
Defaults · values for omitted arguments
trait Defaults[A]:
def hasDefault(index: Int): Boolean
def defaultArgument(index: Int): Any
Methods · parameter structure and invocation
trait MethodMirror[T]
extends Defaults[Any]:
type MirroredElemLabels <: Tuple
type MirroredElemTypes <: Tuple
type MirroredResult
def invoke(args: Array[Any]): Any
Metadata in types · defaults and calls in methods
Flagged interfaces · abbreviated · Source
Flagged · generation entry points
transparent inline given ofProduct[T]:
AnnotMirror.Product[T] =
${ macros.AnnotationMacros.annotMirrorProduct[T] }
inline def derived[A]: Defaults[A] =
${ macros.DefaultMacros.defaults[A] }
transparent inline given of[T]: MethodsMirror.Of[T] =
${ macros.MethodMacros.methodsMirror[T] }
Today: macros discover the source metadata and generate witnesses.
Proposal: the compiler synthesizes these witnesses, as it does for scala.deriving.Mirror.
Library authors consume a shared interface through ordinary inline derivation.
Implemented in Flagged · Proposed compiler facility · API still a candidate design
Annotation occurrence
case class Greet(
@opt(short='w') who: String)
case class opt(
name: String = "",
help: String = "",
short: Char = '\u0000',
// ... more parameters
) extends StaticAnnotation derives Defaults
Annotation Mirror reflects explicit literal arguments:
ArgumentList[opt,
"short" *: EmptyTuple, 'w' *: EmptyTuple,
2 *: EmptyTuple]
Only short is supplied.
how do we materialise the other params?
name and help use their default.
short is the third parameter (index 2).
Defaults mirror provides arguments.
Capture the supplied argument label and literal value.
short is the third parameter (index 2).
Defaults mirror provides the rest.
Flagged implementation · Constant metadata represented as types
@opt(short='w')Annots.fieldAnnotsOfSome · non-empty arguments, abridged
inline erasedValue[m.MirroredAnnotations] match
case _: (ArgumentList[opt,
ns, vs, ?] *: ?) =>
collectField[ns, vs](
name = "", short = MaybeChar.empty, help = "",
positional = false, …)
Annots.collectField · selected cases
inline erasedValue[(Ns, Vs)] match
case _: ("short" *: nt, v *: vt) =>
collectField[nt, vt](
name, MaybeChar(constValue[v & Char]),
help, positional, hidden,
group, aliases, split, greedy)
case _: (EmptyTuple, ?) =>
FieldAnnots(name, short, help, positional,
hidden, group, aliases, split, greedy)
Find @opt in this field's annotations.
Only need the explicit named arguments
traverse the explicit arguments.
shortcut: prefill default arguments
consume short = 'w' argument from head
recursive call on tail types
materialize 'w' from its singleton type.
After explicit arguments are consumed, build one record. Defaults provided in initial call.
Parser extraction uses a sparse inline fold · Reconstructing a full annotation uses AnnotMirror.find + Defaults
Compile one generated CLI source · milliseconds · warm compiler · lower is better
| CLI shape | No parser | Flagged | mainargs | case-app |
|---|---|---|---|---|
| 10 mixed fields | 53.6 ± 4.8 | 100.9 ± 8.6 | 241.1 ± 25.1 | 460.7 ± 34.4 |
| 25 defaulted fields | 47.3 ± 2.4 | 123.2 ± 16.3 | 246.3 ± 19.0 | 502.5 ± 42.5 |
| 3 subcommands | 48.6 ± 4.5 | 108.6 ± 7.1 | 264.9 ± 21.5 | 467.2 ± 65.3 |
| 3 command methods | 29.6 ± 1.4 | 113.6 ± 9.7 | 254.5 ± 43.4 | 497.3 ± 144.2 |
| Docker-style CLI | 58.1 ± 5.8 | 164.3 ± 10.5 | 213.0 ± 25.7 | 520.5 ± 68.6 |
47–106 ms added over the no-parser baseline · lowest compile time of the three libraries in every scenario.
JMH · M3 Max · JDK 25.0.2 · Scala 3.9.0-RC4 · 17 Aug 2026 · Recorded run
Flagged · construct a parser for a Docker-CLI clone and parse the args.
| Parser | Time (µs/op) | Allocation (B/op) |
|---|---|---|
| Flagged | 2.247 ± 0.016 | 13,720 |
| mainargs | 11.482 ± 0.203 | 41,614 |
| scopt | 49.027 ± 0.624 | 107,901 |
| case-app | 51.935 ± 2.442 | 369,246 |
| scallop | 141.698 ± 0.598 | 445,675 |
| picocli | 232.840 ± 2.293 | 396,725 |
5.1× faster than the next parser in this workload.
JMH · M3 Max · JDK 25.0.2 · Scala 3.9.0-RC4 · 17 Aug 2026 · Lower is better · Recorded run
bishabosha / NamedTupleUtil.scala
Check the numeric age, then render the whole row
val row = (Text("Jamie"), Number(42))
require(row(1).value >= 18) // precise per-elem type
render(row)
// Jamie,42
trait Cell { def text: String }
def render[T <: Tuple: ElemsOf[Cell]](cells: T) =
elemsIterator[T, Cell](cells)
.map(_.text).mkString(",")
type ElemsOf[I] =
[T <: Tuple] =>> Tuple.Union[T] <:< I
Keep the type at each position.
row(1) is a Number; its .value is an Int. A Seq[Cell] loses that precise type.
Iterate without casts in the renderer.
With this evidence, elemsIterator returns an Iterator[Cell], so each element has .text.
A heterogeneous tuple.
Text | Number is the union of its element types. Both must be below Cell.
Keep precise element types; use ElemsOf for iteration through their common interface. · Tuple utilities
bishabosha / NamedTupleUtil.scala
Cells in the row; strings in its matching headings
type PersonRow = (name: Text, age: Number)
val row =
(name = Text("Jamie"), age = Number(42))
val titles =
(name = "Full name", age = "Age (years)")
report[PersonRow](row)(titles)
Full name: Jamie
Age (years): 42
Merge two separate sources with different field types, but matching labels and order.
Every row value is a Cell; every heading is a String. How can the generic signature enforce this?
bishabosha / NamedTupleUtil.scala
The report takes cells and a matching record of headings
def report[
Row <: AnyNamedTuple: FieldsOf[Cell]
](row: Row)[
Titles <: SameFields[Row]
: FieldsOf[String]
](titles: Titles): String =
fieldsIterator[Titles, String](titles)
.zip(fieldsIterator[Row, Cell](row)) …
type FieldsOf[I] = [F <: AnyNamedTuple] =>>
ElemsOf[I][NamedTuple.DropNames[F]]
type SameFields[F <: AnyNamedTuple] =
NamedTuple.Map[F, [_] =>> Any]
Prove the input bound.
DropNames exposes the value tuple, so every field must implement Cell.
Preserve labels and order. Forget the original value types; keep the record's fields.
Transform cells into heading strings.
Independently prove that every supplied heading is a String.
Tuple utilities · Record shape and value bounds are separate constraints.

Named tuples for "Bulk data" · Expand image

Refinements for extensible structural types · Expand image
Why can't NamedTuples be as useful as TypeScript's records?
// Invoice.scala
val config = (
invoice = (
id = 5,
period = (start = "2025/1/27", days = 30)
),
listings = (
items = Vector(
(desc = "Premium Insurance (1 year)",
qty = 2, price = 250),
(desc = "Samsung Galaxy S13",
qty = 1, price = 999)
),
taxRate = 12
…
Parsing values from text
How do we extract a date and calculate when payment is due?
Deep nesting
I want to talk about the items, without explicitly redeclaring their type.
Declare a single structure for the deeply nested data
type Invoice = (
invoice: (
id: Int,
period: (start: String, days: Int)
),
listings: (
items: Vector[
(desc: String, qty: Double, price: Int)
],
taxRate: Int,
)
…
"Zoom" into deep-nesting via match types.
1 · Select the record
type Listings =
SelectField[Invoice, "listings"]
2 · Select its collection
type Items =
SelectField[Listings, "items"]
3 · Select the element type
type Item = SelectElem[Items]
type Item stays in sync with type Invoice
Invoice→Listings→Items→Item
InvoiceSelectField[Invoice, "listings"]SelectField[Listings, "items"]SelectElem[Items]
match-types → algebraic operations for nested data
type SelectField[NT <: AnyNamedTuple, K <: String] =
NT match
case NamedTuple[ns, vs] => Search[K, ns, vs]
type SelectElem[S <: Seq[?]] = S match
case Seq[e] => e
type Search[K <: String, Ns <: Tuple, Vs <: Tuple] =
(Ns, Vs) match
case (K *: _, v *: _) => v
case (_ *: ns, _ *: vs) => Search[K, ns, vs]
Find the field
Walk names and value types together.
Then unwrap its container
The output of one operation feeds the next.
bishabosha / 1 - On Configuration and Named Tuples.md
// Invoice.scala
val config = (
invoice = (
id = 5,
period = (start = "2025/1/27", days = 30)
…
Prototype apply functions directly within deeply nested data
val parser = ParseFrom.identity[Invoice]
.patch(
Patch(_.invoice.period.start)
.to[LocalDate]
)
val application = parser.tryRead(config)
Data provides only text
The invoice needs a date to calculate when payment is due.
Select where change occurs
apply a function at this path.
String → LocalDate
Parse yyyy/M/d; report an invalid date.
bishabosha / 1 - On Configuration and Named Tuples.md
Prototype apply functions directly within deeply nested data
val parser = ParseFrom.identity[Invoice]
.patch(
Patch(_.invoice.period.start)
.to[LocalDate]
)
given ParseFrom[String, LocalDate]:
def tryRead(in: String)
: Validated[LocalDate, Err] =
Validation.validate(…)
Select this nested leaf
Keep days and the rest of the invoice.
String → LocalDate
Parse yyyy/M/d; report an invalid date.
Propagate errors
each patch can have independent errors.
bishabosha/scala-object-notation
type Invoice = (
invoice: (
id: PosInt,
period: (start: LocalDate, days: PosInt)
),
listings: (
items: Vector[
(desc: String, qty: BigDecimal, price: Int)
],
taxRate: Range[0, 100],
)
…
given ReadWriter[PosInt] = …
given ReadWriter[LocalDate] = …
given [A, B] => ReadWriter[Range[A, B]] = …
val text = os.read(
os.pwd/"Invoice.scala"
) // = "val config = (…)"
given ReadWriter[Invoice] = …
scalanotation.Readers
.readDeclsAs[Invoice](text)
Semantic types: No more Int, String.
Validate and transform
Int → PosInt or Range[0, 100];
String → LocalDate
Parse basic values into semantic data, or reject with a error message.
bishabosha/scala-object-notation
def parseDate(s: String):
Result[LocalDate, DecodeError] = …
given ReadWriter[LocalDate] =
summon[ReadWriter[String]]
.bimapResult(parseDate)(_.toString)
Reified Function
How to tear-down/construct type A from an abstract structure.
Mapped Schema
A LocalDate is structurally "a String", but adds validation
bishabosha/scala-object-notation
Invoice.scala · excerpt
val config = (
invoice = (
id = 5,
period = (start = "2025-1-27",
days = 30)
),
// listings unchanged
)
Read using the mapped LocalDate schema
Readers.readDeclAs[Invoice](
text, rootName = "config"
)
4:15: In path '.invoice.period.start':
Text '2025-1-27' could not be parsed
at index 4
The full decoding error: source line and column, nested field path, and the date parser's explanation.
The mapped schema expects yyyy/M/d.
Write "2025/1/27" instead.
bishabosha/scala-object-notation
A custom type deep inside the invoice schema
final class Money(val cents: Int)
type Invoice = (
listings: (
items: Vector[(desc: String,
qty: BigDecimal, price: Money)],
taxRate: Int
)
)
given ReadWriter[Invoice] =
ReadWriter.ofFields[Invoice]
at path '.listings.items[].price':
Could not find ReadWriter[Money].
Derivation points to the missing leaf.[] means an element of the collection.
Supply the leaf codec, then derive again
given ReadWriter[Money] =
summon[ReadWriter[Int]]
.bimap(new Money(_))(_.cents)
bishabosha/scala-object-notation
Interpreters use the structure and these operations to work with your values.
bishabosha/scala-object-notation
ReadWriter[InvoiceSchema]
invoice · named tuple
idPosInt mapped from Int
period · named tuple
startLocalDate mapped from String
daysPosInt mapped from Int
listings · named tuple
items · Vector
Element schema · named tuple
descString
qtyBigDecimal
priceInt
taxRateRange[0, 100] mapped from Int
contextual derivation composes leaf schemas automatically.
bishabosha/scala-object-notation
Declare once
ReadWriter[InvoiceSchema]
RawSchema: structure + cursors + builders
…or another interpreter of the same schema.
Declare your data once. Each interpreter decides what to do with it.
bishabosha/scala-object-notation
data with no known schema, e.g. raw JSON, YAML, etc.
RawSchema.Router[A] · self
Children reuse the same schema
interopable with upickle, scala-yaml, etc.
bishabosha/scala-object-notation
Sample config data structure, we want to supply defaults dynamically.
type Config = (
db: Option[(
host: String,
port: Int,
replicas: Vector[(host: String, port: Int)]
)]
)
given DefaultValues[Config] = DefaultValues.of { c =>
Seq(
c.db := Some(("120.0.0.1", 5432, Vector())),
c.db.some.port := 5432,
c.db.some.replicas.each.port := 5432
)
}
Step into optional/sequence types.
e.g. they provided a host but no port.
Install defaults with Configured[A].withDefaultValues, ReadWriter.withConfig
bishabosha/scala-object-notation
class Path[T, AtField <: Boolean] extends Selectable:
type Fields = NamedTuple.Map[
NamedTuple.From[T],
[F] =>> Path[F, true]
]
def selectDynamic(name: String): Path[?, true] = ...
extension [T](path: Path[T, ?])
def some(using r: OptionLike[T]): Path[r.Inner, false] = ...
def each(using r: VectorLike[T]): Path[r.Inner, false] = ...
extension [T](path: Path[T, true])
def :=(value: T): Binding = ...
1 · Project fields into Path
select a field of T to extend the Path.
DefaultValues.of: c =>
c.db // c.selectDynamic("db")
2 · Unwrap a container
DefaultValues.of: c =>
c.db.some.replicas.each
3 · Install a binding only at a field
DefaultValues.of: c =>
c.db.some.port := 5432
No need for complex match-types to handle field selection.
bishabosha/scala-object-notation
Decode 100 order records from strings. SCON uses SCON text; the others use JSON.
| Decoder | Time (µs/op) | Allocation (B/op) |
|---|---|---|
| jsoniter-scala | 9.594 ± 0.136 | 16,344 |
| zio-blocks | 12.647 ± 0.062 | 16,360 |
| SCON, batched + typed | 17.294 ± 0.062 | 9,696 |
| uPickle | 46.452 ± 0.903 | 113,904 |
Less allocation here; still slower than jsoniter-scala and zio-blocks.
Recorded 0.4.4 preview run · Same data model, different text syntax · Results
SCON as a Result.task testbed.
def decodeField(): Result[Unit, DecodeError] =
Result.task:
decodeInt().check
slots.setInt(0, pullIntValue())
object Result:
…
val done: Result[Unit, Nothing] = Ok(())
Unit returning block
decodeInt() pushes int to slot.
.check breaks to task: on failure.
Update the builder as a side effect.
successful task: block returns done
On happy path: no allocations for successful blocks.
class Record[T <: AnyNamedTuple](private val live: T)
extends Selectable:
type Fields = T
inline def selectDynamic(name: String): Any =
live.productElement(constValue[IndexOf[Name, Fields]])
type Parent = (id: String)
def idOf(meta: Record[Parent]): String = meta.id
val parent = Record((id = "record-slide"))
val slide = parent ++ (layout = "standard")
idOf(parentView(slide)) // "record-slide"
slide.layout // "standard"
def parentView[T <: AnyNamedTuple](r: Record[T])(
using Record.IsSubPrefix[T, Parent]
): Record[Parent] = r.asInstanceOf[Record[Parent]]
Named tuple fields, record interface.
Fields = T gives typed selections such as meta.id.
Access fields by index (shared prefix fields have same offsets)
slide appends (layout: String).
Its prefix is still the fields of parent.
Downcast slide to Parent safely
Proof Parent's fields are the prefix of T's
In a SiteMap, layouts for Doc[Record[Parent]] can consume richer document metadata.
TemplateFunctions.scala
class TemplateFunctions[T <: AnyNamedTuple](
private val functions: Record[T]
) extends Selectable:
type Fields = T
TalkRevealTheme.apply · layout and page settings abbreviated
object TalkRevealTheme:
def apply(fonts: DeckFonts = DeckFonts()) =
RevealTheme.withTemplates(
RevealTheme.defaultTemplates ++
WhiteboardTemplates.templates,
…
)
Templates are a record of functions.
TemplateFunctions[T] wraps Record[T] and exposes its named fields through Selectable.
Extend the record; pass it to the parent. Customise markdown with extra functions.
The parent requires its original fields as a prefix.
RevealTheme · structural types to the max
case class SlideMeta(
id: String, seconds: Int = 5, layout: String
)
class RevealTheme(...) extends Theme:
...
type SiteMap = (
deck: Directory[(
index: Doc[DeckMeta],
`speaker-notes`: Doc[NotesMeta],
slides: Docs[SlideMeta]
)]
)
Each document exports Typed frontmatter
Open notebook · Notebook source · Edit → run → inspect the inferred types
Working with Generic Data
Universal Schema
Ideas
"Direct Style"
boundary/break abstractionsMetaprogramming
scala-reflect ported for Scala 3Toolkit
RPC: write request to server, wait for response
Scala Native · macOS, FreeBSD targets only
registerWrite(kq, socket)
var running = true
while running do
val n = pollEventsForever(kq, events, capacity)
for i <- 0 until n if running do
val event = events(i)
if isWriteEvent(event) then
nioWriteBytes(socket, buf)
if !buf.hasRemaining() then switchToRead(kq, socket)
else if isReadEvent(event) then
buf.clear()
val bytesRead = nioReadBytes(socket, buf)
running = bytesRead != EOF
if bytesRead > 0 then
buf.flip()
drainTo(buf, response)
register WRITE interest
Loop forever
BLOCK in kevent until sockets are ready. Return a batch of events.
Loop through the batch of events.
check for WRITE events
do WRITE I/O
drop WRITE, register READ interest
check for READ events
do READ I/O
drain each chunk to the response.
Kqueue polling · buffer I/O · adapted
bishabosha/scala-object-notation
SCON text
(db = (port = 5432, host = "db"),
weights = Vector(2, 1))
Application types
case class Endpoint(port: Int, host: String)
case class Config(
db: Option[Endpoint], weights: Array[Int]
)
Primitive and reference fields
inside a nested record.
The schema chooses the result:
an optional object and an integer array.
Follow one successful read with pooled state and typed factories enabled.
bishabosha/scala-object-notation
(db = (port = 5432, host = "db"), weights = Vector(2, 1))prims[0]—refs[0]—Borrow BuilderSlots for Endpoint. The cursor reaches the next Int.
5432prims[0]—refs[0]—Read the raw number into the Int cell.
5432prims[0]5432refs[0]—Copy the raw Int into BuilderSlots at prims[0], without boxing.
bishabosha/scala-object-notation
(db = (port = 5432, host = "db"), weights = Vector(2, 1))5432prims[0]5432refs[0]—The cursor advances to the String.
5432@hostprims[0]5432refs[0]—@host"db"Copy a reference '@host' into the Ref cell, a reference to the parsed string object.
5432@hostprims[0]5432refs[0]@host@host"db"Copy @host into the borrowed BuilderSlots at refs[0]. Both slots refer to the same String.
bishabosha/scala-object-notation
(db = (port = 5432, host = "db"), weights = Vector(2, 1))5432@hostprims[0]5432refs[0]@hostEndpoint(port = slots.getInt(0), host = slots.getRef(1))The record is complete. Its borrowed BuilderSlots holds both fields.
5432@hostprims[0]5432refs[0]@hostEndpoint(port = 5432, host = slots.getRef(1))getInt(0) pulls port directly into the constructor argument, without boxing.
5432@hostprims[0]5432refs[0]@hostEndpoint(port = 5432, host = @host)getRef(1) pulls the same String reference for host.
5432@hostprims[0]5432refs[0]@host@endpointEndpoint(port = 5432, host = @host)The factory invokes Endpoint with those two arguments.
5432@endpointprims[0]5432refs[0]@host@endpointEndpoint(port = 5432, host = @host)pushRef publishes the new Endpoint reference in the decoder’s Ref slot.
5432@endpointprims[0]5432refs[0]@host@endpointEndpoint(port = 5432, host = @host)Release BuilderSlots to the pool. Endpoint keeps its values, not the buffer.
bishabosha/scala-object-notation
(db = (port = 5432, host = "db"), weights = Vector(2, 1))5432@endpointrefs[0]—refs[1]—@endpointEndpoint(port = 5432, host = @host)Config.db field expects Option[Endpoint]. So we need to wrap @endpoint.
5432@databaserefs[0]—refs[1]—@databaseSome(@endpoint)@database wraps the existing @endpoint reference in Some(...), stored in the Ref cell.
5432@databaserefs[0]@databaserefs[1]—@databaseSome(@endpoint)Push the @database reference to the BuilderSlots for Config at refs[0].
bishabosha/scala-object-notation
(db = (port = 5432, host = "db"), weights = Vector(2, 1))2@databaseMoving on to weights... Read the first raw element into the same Int cell.
2@database2Append the Int cell's value to the typed array builder.
1@database2The cursor advances. Overwrite the Int cell with 1.
1@database21Append Int cell again. Both array elements are raw Int values.
1@weights21@weights@weightsArray[Int](2, 1)Finish the array. The Ref cell now points to the array object.
bishabosha/scala-object-notation
(db = (port = 5432, host = "db"), weights = Vector(2, 1))1@weightsrefs[0]@databaserefs[1]—@weightsArray[Int](2, 1)The completed array is ready for the Config builder.
1@weightsrefs[0]@databaserefs[1]@weights@weightsArray[Int](2, 1)Copy @weights into the Config's borrowed BuilderSlots at refs[1].
1@weightsrefs[0]@databaserefs[1]@weightsConfig(db = slots.getRef(0), weights = slots.getRef(1))The Config factory is ready to pull both arguments from BuilderSlots.
1@weightsrefs[0]@databaserefs[1]@weightsConfig(db = @database, weights = slots.getRef(1))getRef(0) pulls the completed Option reference for db.
1@weightsrefs[0]@databaserefs[1]@weightsConfig(db = @database, weights = @weights)getRef(1) pulls the completed array reference for weights.
1@weightsrefs[0]@databaserefs[1]@weights@configConfig(db = @database, weights = @weights)Construct Config from those references; no child objects are copied.
1@configrefs[0]@databaserefs[1]@weights@configConfig(db = @database, weights = @weights)pushRef places the completed Config reference in the decoder’s Ref slot.
1@configrefs[0]@databaserefs[1]@weights@configConfig(db = @database, weights = @weights)Return the parent’s BuilderSlots to the pool. Config is independent of it.
bishabosha/scala-object-notation
(db = (port = 5432, host = "db"), weights = Vector(2, 1))1@configResult[Config, DecodeError]Result.Ok(@config)@configConfig(db = @database, weights = @weights)One Ok wrapper at the public boundary, containing the existing @config.