LLM docs

Str

Strings represent text. For example, "Hi!" is a string.

This guide starts at a high level and works down to the in-memory representation of strings and their performance characteristics. For reasons that will be explained later in this guide, some string operations are in the Str module while others (notably capitalization, code points, graphemes, and sorting) are in separate packages. There's also a list of recommendations for when to use code points, graphemes, and UTF-8.

Syntax

The most common way to represent strings is using quotation marks:

"Hello, World!"

Using this syntax, the whole string must go on one line. You can write multiline strings using triple quotes:

text =
    """
    In memory, this string will not have any spaces
    at its start. That's because the first line
    starts at the same indentation level as the
    opening quotation mark. Actually, none of these
    lines will be indented.

        However, this line will be indented!
    """

In triple-quoted strings, both the opening and closing """ must be at the same indentation level. Lines in the string begin at that indentation level; the spaces that indent the multiline string itself are not considered content.

Interpolation

String interpolation is syntax for inserting a string into another string.

name = "Sam"

"Hi, my name is $(name)!"

This will evaluate to the string "Hi, my name is Sam!"

You can put any expression you like inside the parentheses, as long as it's all on one line:

colors = ["red", "green", "blue"]

"The colors are $(colors |> Str.joinWith ", ")!"

Interpolation can be used in multiline strings, but the part inside the parentheses must still be on one line.

Escapes

There are a few special escape sequences in strings:

These work in both single-line and multiline strings. We'll also discuss another escape later, for inserting Unicode code points into a string.

Single quote syntax

Try putting '👩' into roc repl. You should see this:

» '👩'

128105 : Int *

The single-quote ' syntax lets you represent a Unicode code point (discussed in the next section) in source code, in a way that renders as the actual text it represents rather than as a number literal. This lets you see what it looks like in the source code rather than looking at a number.

At runtime, the single-quoted value will be treated the same as an ordinary number literal—in other words, '👩' is syntax sugar for writing 128105. You can verify this in roc repl:

» '👩' == 128105

Bool.true : Bool

Double quotes ("), on the other hand, are not type-compatible with integers—not only because strings can be empty ("" is valid, but '' is not) but also because there may be more than one code point involved in any given string!

There are also some special escape sequences in single-quote strings:

Most often this single-quote syntax is used when writing parsers; most Roc programs never use it at all.

Unicode

Roc strings represent text using Unicode This guide will provide only a basic overview of Unicode (the Unicode glossary has over 500 entries in it), but it will include the most relevant differences between these concepts:

It will also explain why some operations are included in Roc's builtin Str module, and why others are in separate packages like roc-lang/unicode.

Graphemes

Let's start with the following string:

"👩‍👩‍👦‍👦"

Some might call this a "character." After all, in a monospace font, it looks to be about the same width as the letter "A" or the punctuation mark "!"—both of which are commonly called "characters." Unfortunately, the term "character" in programming has changed meanings many times across the years and across programming languages, and today it's become a major source of confusion.

Unicode uses the less ambiguous term grapheme, which it defines as a "user-perceived character" (as opposed to one of the several historical ways the term "character" has been used in programming) or, alternatively, "A minimally distinctive unit of writing in the context of a particular writing system."

By Unicode's definition, each of the following is an individual grapheme:

Note that although grapheme is less ambiguous than character, its definition is still open to interpretation. To address this, Unicode has formally specified text segmentation rules which define grapheme boundaries in precise technical terms. We won't get into those rules here, but since they can change with new Unicode releases, functions for working with graphemes are in the roc-lang/unicode package rather than in the builtin Str module. This allows them to be updated without being blocked on a new release of the Roc language.

Code Points

Every Unicode text value can be broken down into Unicode code points, which are integers between 0 and 285_212_438 that describe components of the text. In memory, every Roc string is a sequence of these integers stored in a format called UTF-8, which will be discussed later.

The string "👩‍👩‍👦‍👦" happens to be made up of these code points:

[128105, 8205, 128105, 8205, 128102, 8205, 128102]

From this we can see that:

Combining Code Points

The reason every other code point in 👩‍👩‍👦‍👦 is 8205 is that code point 8205 joins together other code points. This emoji, known as "Family: Woman, Woman, Boy, Boy", is made by combining several emoji using zero-width joiners—which are represented by code point 8205 in memory, and which have no visual repesentation on their own.

Here are those code points again, this time with comments about what they represent:

[128105] # "👩"
[8205]   # (joiner)
[128105] # "👩"
[8205]   # (joiner)
[128102] # "👦"
[8205]   # (joiner)
[128102] # "👦"

One way to read this is "woman emoji joined to woman emoji joined to boy emoji joined to boy emoji." Without the joins, it would be:

"👩👩👦👦"

With the joins, however, it is instead:

"👩‍👩‍👦‍👦"

Even though 👩‍👩‍👦‍👦 is visually smaller when rendered, it takes up almost twice as much memory as 👩👩👦👦 does! That's because it has all the same code points, plus the zero-width joiners in between them.

String equality and normalization

Besides emoji like 👩‍👩‍👦‍👦, another classic example of multiple code points being combined to render as one grapheme has to do with accent marks. Try putting these two strings into roc repl:

"caf\u(e9)"
"cafe\u(301)"

The \u(e9) syntax is a way of inserting code points into string literals. In this case, it's the same as inserting the hexadecimal number 0xe9 as a code point onto the end of the string "caf". Since Unicode code point 0xe9 happens to be é, the string "caf\u(e9)" ends up being identical in memory to the string "café".

We can verify this too:

» "caf\u(e9)" == "café"

Bool.true : Bool

As it turns out, "cafe\u(301)" is another way to represent the same word. The Unicode code point 0x301 represents a "combining acute accent"—which essentially means that it will add an accent mark to whatever came before it. In this case, since "cafe\u(301)" has an e before the "\u(301)", that e ends up with an accent mark on it and becomes é.

Although these two strings get rendered identically to one another, they are different in memory because their code points are different! We can also confirm this in roc repl:

» "caf\u(e9)" == "cafe\u(301)"

Bool.false : Bool

As you can imagine, this can be a source of bugs. Not only are they considered unequal, they also hash differently, meaning "caf\u(e9)" and "cafe\u(301)" can both be separate entries in the same Set.

One way to prevent problems like these is to perform Unicode normalization, a process which converts conceptually equivalent strings (like "caf\u(e9)" and "cafe\u(301)") into one canonical in-memory representation. This makes equality checks on them pass, among other benefits.

It would be technically possible for Roc to perform string normalization automatically on every equality check. Unfortunately, although some programs might want to treat "caf\u(e9)" and "cafe\u(301)" as equivalent, for other programs it might actually be important to be able to tell them apart. If these equality checks always passed, then there would be no way to tell them apart!

As such, normalization must be performed explicitly when desired. Like graphemes, Unicode normalization rules can change with new releases of Unicode. As such, these functions are in separate packages instead of builtins (normalization is planned to be in roc-lang/unicode in the future, but it has not yet been implemented) so that updates to these functions based on new Unicode releases can happen without waiting on new releases of the Roc language.

Capitalization

We've already seen two examples of Unicode definitions that can change with new Unicode releases: graphemes and normalization. Another is capitalization; these rules can change with new Unicode releases (most often in the form of additions of new languages, but breaking changes to capitalization rules for existing languages are also possible), and so they are not included in builtin Str.

This might seem particularly surprising, since capitalization functions are commonly included in standard libraries. However, it turns out that "capitalizing an arbitrary string" is impossible to do correctly without additional information.

For example, what is the capitalized version of this string?

"i"

Similarly, the correct lowercased version of the string "I" is "i" in English and "ı" in Turkish.

Turkish is not the only language to use this dotless i, and it's an example of how a function which capitalizes strings cannot give correct answers without the additional information of which language's capitalization rules should be used.

Many languages defer to the operating system's localization settings for this information. In that design, calling a program's capitalization function with an input string of "i" might give an answer of "I" on one machine and "İ" on a different machine, even though it was the same program running on both systems. Naturally, this can cause bugs—but more than that, writing tests to prevent bugs like this usually requires extra complexity compared to writing ordinary tests.

In general, Roc programs should give the same answers for the same inputs even when run on different machines. There are exceptions to this (e.g. a program running out of system resources on one machine, while being able to make more progress on a machine that has more resources), but operating system's language localization is not among them.

For these reasons, capitalization functions are not in Str. There is a planned roc-lang package to handle use cases like capitalization and sorting—sorting can also vary by language as well as by things like country—but implementation work has not yet started on this package.

UTF-8

Earlier, we discussed how Unicode code points can be described as U32 integers. However, many common code points are very low integers, and can fit into a U8 instead of needing an entire U32 to represent them in memory. UTF-8 takes advantage of this, using a variable-width encoding to represent code points in 1-4 bytes, which saves a lot of memory in the typical case—especially compared to UTF-16, which always uses at least 2 bytes to represent each code point, or UTF-32, which always uses the maximum 4 bytes.

This guide won't cover all the details of UTF-8, but the basic idea is this:

The specific UTF-8 encoding of these bytes involves using 1 to 5 bits of each byte for metadata about multi-byte sequences.

A valuable feature of UTF-8 is that it is backwards-compatible with the ASCII encoding that was widely used for many years. ASCII existed before Unicode did, and only used the integers 0 to 127 to represent its equivalent of code points. The Unicode code points 0 to 127 represent the same semantic information as ASCII, (e.g. the number 64 represents the letter "A" in both ASCII and in Unicode), and since UTF-8 represents code points 0 to 127 using one byte, all valid ASCII strings can be successfully parsed as UTF-8 without any need for conversion.

Since many textual computer encodings—including CSV, XML, and JSON—do not use any code points above 127 for their delimiters, it is often possible to write parsers for these formats using only Str functions which present UTF-8 as raw U8 sequences, such as Str.walkUtf8 and Str.toUtf8. In the typical case where they do not to need to parse out individual Unicode code points, they can get everything they need from Str UTF-8 functions without needing to depend on other packages.

When to use code points, graphemes, and UTF-8

Deciding when to use code points, graphemes, and UTF-8 can be nonobvious to say the least!

The way Roc organizes the Str module and supporting packages is designed to help answer this question. Every situation is different, but the following rules of thumb are typical:

For this reason (among others), grapheme functions live in roc-lang/unicode rather than in Str. They are more niche than they seem, so they should not be reached for all the time!

Performance

This section deals with how Roc strings are represented in memory, and their performance characteristics.

A normal heap-allocated roc Str is represented on the stack as:

Each of these three fields is the same size: 64 bits on a 64-bit system, and 32 bits on a 32-bit system. The actual contents of the string are stored in one contiguous sequence of bytes, encoded as UTF-8, often on the heap but sometimes elsewhere—more on this later. Empty strings do not have heap allocations, so an empty Str on a 64-bit system still takes up 24 bytes on the stack (due to its three 64-bit fields).

Reference counting and opportunistic mutation

Like lists, dictionaries, and sets, Roc strings are automatically reference-counted and can benefit from opportunistic in-place mutation. The reference count is stored on the heap immediately before the first byte of the string's contents, and it has the same size as a memory address. This means it can count so high that it's impossible to write a Roc program which overflows a reference count, because having that many simultaneous references (each of which is a memory address) would have exhausted the operating system's address space first.

When the string's reference count is 1, functions like Str.concat and Str.replaceEach mutate the string in-place rather than allocating a new string. This preserves semantic immutability because it is unobservable in terms of the operation's output; if the reference count is 1, it means that memory would have otherwise been deallocated immediately anyway, and it's more efficient to reuse it instead of deallocating it and then immediately making a new allocation.

The contents of statically-known strings (today that means string literals) are stored in the readonly section of the binary, so they do not need heap allocations or reference counts. They are not eligible for in-place mutation, since mutating the readonly section of the binary would cause an operating system access violation.

Small String Optimization

Roc uses a "small string optimization" when representing certain strings in memory.

If you have a sufficiently long string, then on a 64-bit system it will be represented on the stack using 24 bytes, and on a 32-bit system it will take 12 bytes—plus however many bytes are in the string itself—on the heap. However, if there is a string shorter than either of these stack sizes (so, a string of up to 23 bytes on a 64-bit system, and up to 11 bytes on a 32-bit system), then that string will be stored entirely on the stack rather than having a separate heap allocation at all.

This can be much more memory-efficient! However, List does not have this optimization (it has some runtime cost, and in the case of List it's not anticipated to come up nearly as often), which means when converting a small string to List U8 it can result in a heap allocation.

Note that this optimization is based entirely on how many UTF-8 bytes the string takes up in memory. It doesn't matter how many graphemes, code points or anything else it has; the only factor that determines whether a particular string is eligible for the small string optimization is the number of UTF-8 bytes it takes up in memory!

Seamless Slices

Try putting this into roc repl:

» "foo/bar/baz" |> Str.splitOn "/"

["foo", "bar", "baz"] : List Str

All of these strings are small enough that the small string optimization will apply, so none of them will be allocated on the heap.

Now let's suppose they were long enough that this optimization no longer applied:

» "a much, much, much, much/longer/string compared to the last one!" |> Str.splitOn "/"

["a much, much, much, much", "longer", "string compared to the last one!"] : List Str

Here, the only strings small enough for the small string optimization are "/" and "longer". They will be allocated on the stack.

The first and last strings in the returned list "a much, much, much, much" and "string compared to the last one!" will not be allocated on the heap either. Instead, they will be seamless slices, which means they will share memory with the original input string.

All of these strings are semantically immutable, so sharing these bytes is an implementation detail that should only affect performance. By design, there is no way at either compile time or runtime to tell whether a string is a seamless slice. This allows the optimization's behavior to change in the future without affecting Roc programs' semantic behavior.

Seamless slices create additional references to the original string, which make it ineligible for opportunistic mutation (along with the slices themselves; slices are never eligible for mutation), and which also make it take longer before the original string can be deallocated. A case where this might be noticeable in terms of performance would be:

  1. A function takes a very large string as an argument and returns a much smaller slice into that string.
  2. The smaller slice is used for a long time in the program, whereas the much larger original string stops being used.
  3. In this situation, it might have been better for total program memory usage (although not necessarily overall performance) if the original large string could have been deallocated sooner, even at the expense of having to copy the smaller string into a new allocation instead of reusing the bytes with a seamless slice.

If a situation like this comes up, a slice can be turned into a separate string by using Str.concat to concatenate the slice onto an empty string (or one created with Str.withCapacity).

Currently, the only way to get seamless slices of strings is by calling certain Str functions which return them. In general, Str functions which accept a string and return a subset of that string tend to do this. Str.trim is another example of a function which returns a seamless slice.

Utf8ByteProblem : [ InvalidStartByte, UnexpectedEndOfSequence, ExpectedContinuation, OverlongEncoding, CodepointTooLarge, EncodesSurrogateHalf ]

Utf8Problem

isEmpty : Str -> Bool

Returns Bool.true if the string is empty, and Bool.false otherwise.

expect Str.isEmpty "hi!" == Bool.false
expect Str.isEmpty "" == Bool.true

concat : Str, Str -> Str

Concatenates two strings together.

expect Str.concat "ab" "cd" == "abcd"
expect Str.concat "hello" "" == "hello"
expect Str.concat "" "" == ""

withCapacity : U64 -> Str

Returns a string of the specified capacity without any content.

This is a performance optimization tool that's like calling Str.reserve on an empty string. It's useful when you plan to build up a string incrementally, for example by calling Str.concat on it:

greeting = "Hello and welcome to Roc"
subject = "Awesome Programmer"

# Evaluates to "Hello and welcome to Roc, Awesome Programmer!"
helloWorld =
    Str.withCapacity 45
    |> Str.concat greeting
    |> Str.concat ", "
    |> Str.concat subject
    |> Str.concat "!"

In general, if you plan to use Str.concat on an empty string, it will be faster to start with Str.withCapacity than with "". Even if you don't know the exact capacity of the string, giving withCapacity a higher value than ends up being necessary can help prevent reallocation and copying—at the cost of using more memory than is necessary.

For more details on how the performance optimization works, see Str.reserve.

reserve : Str, U64 -> Str

Increase a string's capacity by at least the given number of additional bytes.

This can improve the performance of string concatenation operations like Str.concat by allocating extra capacity up front, which can prevent the need for reallocations and copies. Consider the following example which does not use Str.reserve:

greeting = "Hello and welcome to Roc"
subject = "Awesome Programmer"

# Evaluates to "Hello and welcome to Roc, Awesome Programmer!"
helloWorld =
    greeting
    |> Str.concat ", "
    |> Str.concat subject
    |> Str.concat "!"

In this example:

  1. We start with greeting, which has both a length and capacity of 24 (bytes).
  2. |> Str.concat ", " will see that there isn't enough capacity to add 2 more bytes for the ", ", so it will create a new heap allocation with enough bytes to hold both. (This probably will be more than 7 bytes, because when Str functions reallocate, they apply a multiplier to the exact capacity required. This makes it less likely that future realloctions will be needed. The multiplier amount is not specified, because it may change in future releases of Roc, but it will likely be around 1.5 to 2 times the exact capacity required.) Then it will copy the current bytes ("Hello") into the new allocation, and finally concatenate the ", " into the new allocation. The old allocation will then be deallocated because it's no longer referenced anywhere in the program.
  3. |> Str.concat subject will again check if there is enough capacity in the string. If it doesn't find enough capacity once again, it will make a third allocation, copy the existing bytes ("Hello, ") into that third allocation, and then deallocate the second allocation because it's already no longer being referenced anywhere else in the program. (It may find enough capacity in this particular case, because the previous Str.concat allocated something like 1.5 to 2 times the necessary capacity in order to anticipate future concatenations like this...but if something longer than "World" were being concatenated here, it might still require further reallocation and copying.)
  4. |> Str.concat "!\n" will repeat this process once more.

This process can have significant performance costs due to multiple reallocation of new strings, copying between old strings and new strings, and deallocation of immediately obsolete strings.

Here's a modified example which uses Str.reserve to eliminate the need for all that reallocation, copying, and deallocation.

helloWorld =
    greeting
    |> Str.reserve 21
    |> Str.concat ", "
    |> Str.concat subject
    |> Str.concat "!"

In this example:

  1. We again start with greeting, which has both a length and capacity of 24 bytes.
  2. |> Str.reserve 21 will ensure that there is enough capacity in the string for an additional 21 bytes (to make room for ", ", "Awesome Programmer", and "!"). Since the current capacity is only 24, it will create a new 45-byte (24 + 21) heap allocation and copy the contents of the existing allocation (greeting) into it.
  3. |> Str.concat ", " will concatenate , to the string. No reallocation, copying, or deallocation will be necessary, because the string already has a capacity of 45 btytes, and greeting will only use 24 of them.
  4. |> Str.concat subject will concatenate subject ("Awesome Programmer") to the string. Again, no reallocation, copying, or deallocation will be necessary.
  5. |> Str.concat "!\n" will concatenate "!\n" to the string, still without any reallocation, copying, or deallocation.

Here, Str.reserve prevented multiple reallocations, copies, and deallocations during the Str.concat calls. Notice that it did perform a heap allocation before any Str.concat calls were made, which means that using Str.reserve is not free! You should only use it if you actually expect to make use of the extra capacity.

Ideally, you'd be able to predict exactly how many extra bytes of capacity will be needed, but this may not always be knowable. When you don't know exactly how many bytes to reserve, you can often get better performance by choosing a number of bytes that's too high, because a number that's too low could lead to reallocations. There's a limit to this, of course; if you always give it ten times what it turns out to need, that could prevent reallocations but will also waste a lot of memory!

If you plan to use Str.reserve on an empty string, it's generally better to use Str.withCapacity instead.

joinWith : List Str, Str -> Str

Combines a List of strings into a single string, with a separator string in between each.

expect Str.joinWith ["one", "two", "three"] ", " == "one, two, three"
expect Str.joinWith ["1", "2", "3", "4"] "." == "1.2.3.4"

splitOn : Str, Str -> List Str

Split a string around a separator.

Passing "" for the separator is not useful; it returns the original string wrapped in a List.

expect Str.splitOn "1,2,3" "," == ["1","2","3"]
expect Str.splitOn "1,2,3" "" == ["1,2,3"]

repeat : Str, U64 -> Str

Repeats a string the given number of times.

expect Str.repeat "z" 3 == "zzz"
expect Str.repeat "na" 8 == "nananananananana"

Returns "" when given "" for the string or 0 for the count.

expect Str.repeat "" 10 == ""
expect Str.repeat "anything" 0 == ""

toUtf8 : Str -> List U8

Returns a List of the string's U8 UTF-8 code units. (To split the string into a List of smaller Str values instead of U8 values, see Str.splitOn.)

expect Str.toUtf8 "Roc" == [82, 111, 99]
expect Str.toUtf8 "鹏" == [233, 185, 143]
expect Str.toUtf8 "சி" == [224, 174, 154, 224, 174, 191]
expect Str.toUtf8 "🐦" == [240, 159, 144, 166]

fromUtf8 : List U8 -> Result Str [BadUtf8 Utf8ByteProblem U64]

Converts a List of U8 UTF-8 code units to a string.

Returns Err if the given bytes are invalid UTF-8, and returns Ok "" when given [].

expect Str.fromUtf8 [82, 111, 99] == Ok "Roc"
expect Str.fromUtf8 [233, 185, 143] == Ok "鹏"
expect Str.fromUtf8 [224, 174, 154, 224, 174, 191] == Ok "சி"
expect Str.fromUtf8 [240, 159, 144, 166] == Ok "🐦"
expect Str.fromUtf8 [] == Ok ""
expect Str.fromUtf8 [255] |> Result.isErr

startsWith : Str, Str -> Bool

Check if the given Str starts with a value.

expect Str.startsWith "ABC" "A" == Bool.true
expect Str.startsWith "ABC" "X" == Bool.false

endsWith : Str, Str -> Bool

Check if the given Str ends with a value.

expect Str.endsWith "ABC" "C" == Bool.true
expect Str.endsWith "ABC" "X" == Bool.false

trim : Str -> Str

Return the Str with all whitespace removed from both the beginning as well as the end.

expect Str.trim "   Hello      \n\n" == "Hello"

trimStart : Str -> Str

Return the Str with all whitespace removed from the beginning.

expect Str.trimStart "   Hello      \n\n" == "Hello      \n\n"

trimEnd : Str -> Str

Return the Str with all whitespace removed from the end.

expect Str.trimEnd "   Hello      \n\n" == "   Hello"

toDec : Str -> Result Dec [InvalidNumStr]

Encode a Str to a Dec. A Dec value is a 128-bit decimal fixed-point number.

expect Str.toDec "10" == Ok 10dec
expect Str.toDec "-0.25" == Ok -0.25dec
expect Str.toDec "not a number" == Err InvalidNumStr

toF64 : Str -> Result F64 [InvalidNumStr]

Encode a Str to a F64. A F64 value is a 64-bit floating-point number and can be specified with a f64 suffix.

expect Str.toF64 "0.10" == Ok 0.10f64
expect Str.toF64 "not a number" == Err InvalidNumStr

toF32 : Str -> Result F32 [InvalidNumStr]

Encode a Str to a F32.A F32 value is a 32-bit floating-point number and can be specified with a f32 suffix.

expect Str.toF32 "0.10" == Ok 0.10f32
expect Str.toF32 "not a number" == Err InvalidNumStr

toU128 : Str -> Result U128 [InvalidNumStr]

Encode a Str to an unsigned U128 integer. A U128 value can hold numbers from 0 to 340_282_366_920_938_463_463_374_607_431_768_211_455 (over 340 undecillion). It can be specified with a u128 suffix.

expect Str.toU128 "1500" == Ok 1500u128
expect Str.toU128 "0.1" == Err InvalidNumStr
expect Str.toU128 "-1" == Err InvalidNumStr
expect Str.toU128 "not a number" == Err InvalidNumStr

toI128 : Str -> Result I128 [InvalidNumStr]

Encode a Str to a signed I128 integer. A I128 value can hold numbers from -170_141_183_460_469_231_731_687_303_715_884_105_728 to 170_141_183_460_469_231_731_687_303_715_884_105_727. It can be specified with a i128 suffix.

expect Str.toI128 "1500" == Ok 1500i128
expect Str.toI128 "-1" == Ok -1i128
expect Str.toI128 "0.1" == Err InvalidNumStr
expect Str.toI128 "not a number" == Err InvalidNumStr

toU64 : Str -> Result U64 [InvalidNumStr]

Encode a Str to an unsigned U64 integer. A U64 value can hold numbers from 0 to 18_446_744_073_709_551_615 (over 18 quintillion). It can be specified with a u64 suffix.

expect Str.toU64 "1500" == Ok 1500u64
expect Str.toU64 "0.1" == Err InvalidNumStr
expect Str.toU64 "-1" == Err InvalidNumStr
expect Str.toU64 "not a number" == Err InvalidNumStr

toI64 : Str -> Result I64 [InvalidNumStr]

Encode a Str to a signed I64 integer. A I64 value can hold numbers from -9_223_372_036_854_775_808 to 9_223_372_036_854_775_807. It can be specified with a i64 suffix.

expect Str.toI64 "1500" == Ok 1500i64
expect Str.toI64 "-1" == Ok -1i64
expect Str.toI64 "0.1" == Err InvalidNumStr
expect Str.toI64 "not a number" == Err InvalidNumStr

toU32 : Str -> Result U32 [InvalidNumStr]

Encode a Str to an unsigned U32 integer. A U32 value can hold numbers from 0 to 4_294_967_295 (over 4 billion). It can be specified with a u32 suffix.

expect Str.toU32 "1500" == Ok 1500u32
expect Str.toU32 "0.1" == Err InvalidNumStr
expect Str.toU32 "-1" == Err InvalidNumStr
expect Str.toU32 "not a number" == Err InvalidNumStr

toI32 : Str -> Result I32 [InvalidNumStr]

Encode a Str to a signed I32 integer. A I32 value can hold numbers from -2_147_483_648 to 2_147_483_647. It can be specified with a i32 suffix.

expect Str.toI32 "1500" == Ok 1500i32
expect Str.toI32 "-1" == Ok -1i32
expect Str.toI32 "0.1" == Err InvalidNumStr
expect Str.toI32 "not a number" == Err InvalidNumStr

toU16 : Str -> Result U16 [InvalidNumStr]

Encode a Str to an unsigned U16 integer. A U16 value can hold numbers from 0 to 65_535. It can be specified with a u16 suffix.

expect Str.toU16 "1500" == Ok 1500u16
expect Str.toU16 "0.1" == Err InvalidNumStr
expect Str.toU16 "-1" == Err InvalidNumStr
expect Str.toU16 "not a number" == Err InvalidNumStr

toI16 : Str -> Result I16 [InvalidNumStr]

Encode a Str to a signed I16 integer. A I16 value can hold numbers from -32_768 to 32_767. It can be specified with a i16 suffix.

expect Str.toI16 "1500" == Ok 1500i16
expect Str.toI16 "-1" == Ok -1i16
expect Str.toI16 "0.1" == Err InvalidNumStr
expect Str.toI16 "not a number" == Err InvalidNumStr

toU8 : Str -> Result U8 [InvalidNumStr]

Encode a Str to an unsigned U8 integer. A U8 value can hold numbers from 0 to 255. It can be specified with a u8 suffix.

expect Str.toU8 "250" == Ok 250u8
expect Str.toU8 "-0.1" == Err InvalidNumStr
expect Str.toU8 "not a number" == Err InvalidNumStr
expect Str.toU8 "1500" == Err InvalidNumStr

toI8 : Str -> Result I8 [InvalidNumStr]

Encode a Str to a signed I8 integer. A I8 value can hold numbers from -128 to 127. It can be specified with a i8 suffix.

expect Str.toI8 "-15" == Ok -15i8
expect Str.toI8 "150.00" == Err InvalidNumStr
expect Str.toI8 "not a number" == Err InvalidNumStr

countUtf8Bytes : Str -> U64

Gives the number of bytes in a Str value.

expect Str.countUtf8Bytes "Hello World" == 11

replaceEach : Str, Str, Str -> Str

Returns the given Str with each occurrence of a substring replaced. If the substring is not found, returns the original string.

expect Str.replaceEach "foo/bar/baz" "/" "_" == "foo_bar_baz"
expect Str.replaceEach "not here" "/" "_" == "not here"

replaceFirst : Str, Str, Str -> Str

Returns the given Str with the first occurrence of a substring replaced. If the substring is not found, returns the original string.

expect Str.replaceFirst "foo/bar/baz" "/" "_" == "foo_bar/baz"
expect Str.replaceFirst "no slashes here" "/" "_" == "no slashes here"

replaceLast : Str, Str, Str -> Str

Returns the given Str with the last occurrence of a substring replaced. If the substring is not found, returns the original string.

expect Str.replaceLast "foo/bar/baz" "/" "_" == "foo/bar_baz"
expect Str.replaceLast "no slashes here" "/" "_" == "no slashes here"

splitFirst : Str, Str -> Result { before : Str, after : Str } [NotFound]

Returns the given Str before the first occurrence of a delimiter, as well as the rest of the string after that occurrence. Returns [Err NotFound] if the delimiter is not found.

expect Str.splitFirst "foo/bar/baz" "/" == Ok { before: "foo", after: "bar/baz" }
expect Str.splitFirst "no slashes here" "/" == Err NotFound

splitLast : Str, Str -> Result { before : Str, after : Str } [NotFound]

Returns the given Str before the last occurrence of a delimiter, as well as the rest of the string after that occurrence. Returns [Err NotFound] if the delimiter is not found.

expect Str.splitLast "foo/bar/baz" "/" == Ok { before: "foo/bar", after: "baz" }
expect Str.splitLast "no slashes here" "/" == Err NotFound

walkUtf8WithIndex : Str, state, (state, U8, U64 -> state) -> state

Walks over the UTF-8 bytes of the given Str and calls a function to update state for each byte. The index for that byte in the string is provided to the update function.

f : List U8, U8, U64 -> List U8
f = \state, byte, _ -> List.append state byte
expect Str.walkUtf8WithIndex "ABC" [] f == [65, 66, 67]

walkUtf8 : Str, state, (state, U8 -> state) -> state

Walks over the UTF-8 bytes of the given Str and calls a function to update state for each byte.

sumOfUtf8Bytes =
    Str.walkUtf8 "Hello, World!" 0 \total, byte ->
        total + byte

expect sumOfUtf8Bytes == 105

releaseExcessCapacity : Str -> Str

Shrink the memory footprint of a str such that its capacity and length are equal. Note: This will also convert seamless slices to regular lists.

withPrefix : Str, Str -> Str

Adds a prefix to the given Str.

expect Str.withPrefix "Awesome" "Roc" == "RocAwesome"

contains : Str, Str -> Bool

Determines whether or not the first Str contains the second.

expect Str.contains "foobarbaz" "bar"
expect !(Str.contains "apple" "orange")
expect Str.contains "anything" ""

dropPrefix : Str, Str -> Str

Drops the given prefix Str from the start of a Str If the prefix is not found, returns the original string.

expect Str.dropPrefix "bar" "foo" == "bar"
expect Str.dropPrefix "foobar" "foo" == "bar"

dropSuffix : Str, Str -> Str

Drops the given suffix Str from the end of a Str If the suffix is not found, returns the original string.

expect Str.dropSuffix "bar" "foo" == "bar"
expect Str.dropSuffix "barfoo" "foo" == "bar"