Skip to content

standardize all stringification API's around allocation-free binary $ operator + variadic strAppend #191

Open
@timotheecour

Description

@timotheecour

API's like $(a: MyType) (which are what we almost exclusively use in stdlib, nimble etc) do not compose efficiently because they create temporaries, allocate etc.
instead, we should build API's around appending. This gives best of both worlds:

  • efficiency where needed
  • still possible to use old way $a wherever it's convenient, thanks a single unary $ overload:
template `$`*[T](a: T): string =
  ## the only unary `$` that's ever needed
  var result: string
  result$a
  result

note

outplace operator is not relevant to this discussion, we don't want to use outplace operator for something so common like stringification

proposal part 1: binary $ operator

the API to stringify MyType shall be:

proc `$`(result: var string, a: MyType)

example

import system except `$`

## overloads of binary `$` for all types we need to support
proc `$`*(result: var string, a: float) = result.addFloat a
proc `$`*(result: var string, a: int) = result.addInt a
template `$`*(result: var string, a: string) = result.add a

template `$`*[T](a: T): string =
  ## the only unary `$` that's ever needed
  var result: string
  result$a
  result

proposal part 2: variadic strAppend

the following variadic strAppend is always preferable to & + $, and often preferable to varargs[string,$]

following macro strAppend advantageously replaces many patterns, yet is as efficient as can be.
In particular, it allows writing a more efficient echo(a: varargs[string, $]) and write(a: File, args: varargs[string, $]), without introducing a bunch of temporaries.

import macros

macro strAppend*(dst: var string, args: varargs[untyped]): void =
  result = newStmtList()
  for ai in args:
    result.add quote do:
      `dst`$`ai`

macro strAppend*(args: varargs[untyped]): string =
  result = newStmtList()
  let dst = genSym(nskVar, "dst")
  result.add quote do:
    var `dst`: string
  for ai in args:
    result.add quote do:
      `dst`$`ai`
  result.add quote do:
    `dst`

these advantageously replace varargs[string,$] versions of system.echo, system.write:

template echo*(args: varargs[untyped]) =
  ## more efficient than system.echo: no temporaries
  ## could be further optimized using a fixed size buffer when short enough or even via a single threadvar string to do all the appending
  system.write(stdout, strAppend(args, "\n"))

template write*(f: File, a: varargs[untyped]) =
  ## more efficient than `write*(f: File, a: varargs[string, `$`]` in system/io
  system.write(f, strAppend(a))

usage

proc main()=
  var x = 0.3
  var ret: string
  ret$x
  ret$" bar "
  ret$12
  doAssert ret == "0.3 bar 12"
  doAssert $0.3 == "0.3"

  var ret2: string
  ret2.strAppend 0.1, " foo ", 12
  doAssert ret2 == "0.1 foo 12"

  doAssert strAppend(0.1, " foo ", 12) == "0.1 foo 12"
  echo 0.1, " foo ", 12 # prints `0.1 foo 12`

  stdout.write 0.1, " to_stdout ", 12, "\n" # prints 0.1 to_stdout 12

main()

note

I have a WIP that would allow writing strAppend as a template instead of a macro, to avoid depending on macros.nim; in particular that means that strAppend and echo thus defined could be in system.nim (or alternatively for system.nim minimalists, hiding strAppend there, using it to redefine echo, and redefining strAppend in some other module, but IMO it just belongs in system since its ubiquitous, replacing "foo" & $bar both performance wise and syntactically, using less operator noise (unlike both using & and $)).

the way it works is it allows templates to iterate over a varargs.

links

  • when done, remove compiler/strutils2

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions