Better multiline string templates

Subtle problems with templates in multiline strings and some ways to fix them

These days many languages support multiline string templates. Here's what they look like in Kotlin:

val example = """
        This
        is a
        multiline
        string.
    """

But that will create a multiline with a bunch of spaces at the beginning of each line:

        This
        is a
        multiline
        string.

So I like to use trimIndent to remove these spaces.

val example = """
        This
        is a
        multiline
        string.
    """.trimIndent()

This works the way I expect:

This
is a
multiline
string.

But problems happen when I combine string templating with trimIndent:

val center = """
        is a
        multiline
    """.trimIndent()

val example = """
        This
        $center
        string.
    """.trimIndent()

$center gets expanded to a string consisting of two lines with no indentation. The first line is indented because $center itself is indented. But the second line is not indented.

        This
        is a
multiline
        string.

If you want a workaround that works in kotlin, try this:

val center = """
       |is a
       |multiline
    """.trimMargin()

val example = """
       |This
       |${center.replace("\n", "\n|")}
       |string.
    """.trimMargin()

I think the margin character is a little ugly, and the requirement of embedding with ${center.replace("\n", "\n|")} is quite ugly. Maybe even worse, you can use $center and it will actually work - most of the time. The problem happens if center contains lines that actually begin with the | character:

val center = """
       |is a
       ||multiline beginning with pipe
    """.trimMargin()

val example = """
       |This
       |$center
       |string.
    """.trimMargin()

Notice that extra | I added in front of multiline? That indicates that the line actually does begin with a | character:

is a
|multiline beginning with pipe

But because I referenced $center directly that leading | gets removed and |multiline beginning with pipe turns into multiline beginning with pipe:

This
is a
multiline beginning with pipe
string.

So it works, if you accept some ugliness, and you're careful. I wanted a better way. And it turns out I can do it in Scala. I just need to define a custom string type:

implicit class AdjustedMultiline(val sc: StringContext) extends AnyVal {
    def m(args: Any*): String = {
        // Create a string where each arg is 'x'
        assert(sc.parts.length == args.length + 1)
        val parts = sc.parts
        val substitutedArgs = args.map(_ => "x")
        val indentString = (parts.init zip substitutedArgs).map { case (part, arg) => part + arg }.mkString + parts.last

        // Use this string to compute the indent count
        val indentCount = indentString.linesIterator
            .filter(_.trim.nonEmpty)
            .map(line => line.takeWhile(_ == ' ').length)
            .minOption
            .getOrElse(0) // Default value when there's no non-empty line

        // Add in this indentation to each newline within each arg
        val processed = sc.parts.zipAll(args, "", "").map {
            case (str, arg) =>
                str + arg.toString.split("\n").mkString("\n" + (" " * indentCount))
        }

        // Strip starting/ending newlines with leading/trailing spaces.
        // We do this to support syntax like:
        //   val str = m"""
        //       string content
        //     """
        // which should be equal to "string content"
        val combined = processed.mkString.replaceAll("^ *\n", "").replaceAll("\n *$", "")

        combined.linesIterator.map(line => line.drop(indentCount)).mkString("\n")
    }
}

And then I can write:

val center = m"""
        is a
        multiline
    """

val example = m"""
        This
        $center
        string.
    """

And it works the way I'd hope:

This
is a
multiline
string.

Is there any reason why language designers don't handle multiline string templating like this by default?


If you enjoyed this post, please let me know on Twitter or Bluesky.

Posted November 21, 2023.

Tags: #misc