Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Strings

In LFE, as in Erlang, strings are simply lists of integers representing character codes. This might seem unusual if you're coming from languages with dedicated string types, but it's a perfectly sensible representation that fits naturally with list processing. A string is just a list where each element happens to be an integer in the valid character range.

Strings vs. Binaries

While strings-as-lists are the traditional representation, modern Erlang and LFE code increasingly uses binaries for text due to their superior memory efficiency and performance with large text. Binaries will be covered in detail in a later chapter. For now, we focus on list-based strings as they're simpler to understand and still widely used for smaller text operations.

Creating Strings

String literals are enclosed in double quotes:

lfe> "Hello, World!"
"Hello, World!"
lfe> "The answer is 42"
"The answer is 42"
lfe> ""
""

Under the hood, these are lists of integers:

lfe> (== "ABC" '(65 66 67))
true
lfe> (== "ABC" (list 65 66 67))
true

You can see this more clearly by forcing LFE to show the underlying representation:

lfe> (io:format "~w~n" '("ABC"))
[65,66,67]
ok

Character literals make this relationship explicit:

lfe> (list #\A #\B #\C)
"ABC"
lfe> #\A
65

String Operations

Since strings are lists, all list operations work on strings:

lfe> (++ "Hello, " "World!")
"Hello, World!"
lfe> (lists:reverse "stressed")
"desserts"
lfe> (length "Hello")
5

Accessing characters:

lfe> (car "Hello")
72
lfe> (cdr "Hello")
"ello"
lfe> (lists:nth 2 "Hello")
101

Case conversion:

lfe> (string:uppercase "hello world")
"HELLO WORLD"
lfe> (string:lowercase "HELLO WORLD")
"hello world"
lfe> (string:titlecase "hello world")
"Hello World"

Trimming whitespace:

lfe> (string:trim "  hello  ")
"hello"
lfe> (string:trim "  hello  " 'leading)
"hello  "
lfe> (string:trim "  hello  " 'trailing)
"  hello"

Splitting and joining:

lfe> (string:split "one,two,three" "," 'all)
("one" "two" "three")
lfe> (string:split "a:b:c:d" ":")
("a" "b:c:d")
lfe> (string:split "a:b:c:d" ":" 'all)
("a" "b" "c" "d")

lfe> (string:join '("one" "two" "three") ", ")
"one, two, three"

Searching:

lfe> (string:find "hello world" "world")
"world"
lfe> (string:find "hello world" "universe")
'nomatch

lfe> (string:str "hello world" "world")
7
lfe> (string:str "hello world" "universe")
0

Note that string:str returns 1-based position or 0 if not found.

Replacing:

lfe> (string:replace "hello world" "world" "universe")
"hello universe"
lfe> (string:replace "a,b,c,d" "," ":" 'all)
"a:b:c:d"

Checking prefixes and suffixes:

lfe> (string:prefix "hello world" "hello")
"world"
lfe> (string:prefix "hello world" "goodbye")
'nomatch

lfe> (lists:prefix "hello" "hello world")
true
lfe> (lists:prefix "goodbye" "hello world")
false

lfe> (lists:suffix "world" "hello world")
true

String Predicates

Testing for empty strings:

lfe> (string:is_empty "")
true
lfe> (string:is_empty "hello")
false

Testing if all characters match a predicate:

lfe> (lists:all (lambda (c) (and (>= c #\a) (=< c #\z))) "hello")
true
lfe> (lists:all (lambda (c) (and (>= c #\a) (=< c #\z))) "Hello")
false

Formatting Strings

The format family of functions provides powerful string interpolation:

lfe> (io_lib:format "The answer is ~p" '(42))
"The answer is 42"

lfe> (io_lib:format "~s ~s ~p" '("Hello" "world" 123))
"Hello world 123"

lfe> (io_lib:format "~.2f" '(3.14159))
"3.14"

lfe> (io_lib:format "~10.2.0f" '(3.14159))
"      3.14"

Common format specifiers:

  • ~p — pretty-print any term
  • ~s — string
  • ~w — write in Erlang term format
  • ~c — character
  • ~f — float
  • ~e — exponential notation
  • ~.Nf — float with N decimal places
  • ~n — newline

For console output, use lfe_io:format or io:format:

lfe> (lfe_io:format "Hello, ~s!~n" '("World"))
Hello, World!
ok

lfe> (io:format "The answer is ~p~n" '(42))
The answer is 42
ok

Converting Between Types

Atoms to strings:

lfe> (atom_to_list 'hello)
"hello"
lfe> (atom_to_list 'hello-world)
"hello-world"

Strings to atoms:

lfe> (list_to_atom "hello")
hello
lfe> (list_to_existing_atom "hello")
hello

Caution: Atom Creation

Remember that atoms are not garbage collected. Use list_to_atom cautiously with user input, as creating too many atoms can exhaust the atom table and crash the VM. Prefer list_to_existing_atom when you expect the atom to already exist.

Numbers to strings:

lfe> (integer_to_list 42)
"42"
lfe> (integer_to_list 42 16)
"2A"

lfe> (float_to_list 3.14159)
"3.14159000000000009237e+00"
lfe> (float_to_list 3.14159 '(#(decimals 2)))
"3.14"

Strings to numbers:

lfe> (list_to_integer "42")
42
lfe> (list_to_integer "2A" 16)
42

lfe> (list_to_float "3.14159")
3.14159

Binaries to strings:

lfe> (binary_to_list #"Hello")
"Hello"
lfe> (list_to_binary "Hello")
#"Hello"

Unicode Support

LFE strings can contain Unicode characters, though the integer representation may be surprising:

lfe> "Hello, 世界"
"Hello, 世界"

lfe> (length "Hello, 世界")
9

lfe> (lists:nth 8 "Hello, 世界")
19990

For proper Unicode string handling, especially with grapheme clusters, use the Unicode-aware functions in the string module:

lfe> (string:length "Hello, 世界")
9

lfe> (string:slice "Hello, 世界" 7 2)
"世界"

The modern string module (Erlang 20+) handles Unicode correctly and should be preferred over older list-based string functions when working with international text.

Common Patterns

Building strings incrementally:

(defun build-message (name age)
  (++ "Name: " name ", Age: " (integer_to_list age)))

Processing each character:

(defun count-vowels
  (("") 0)
  (((cons c rest))
   (let ((is-vowel (lists:member c "aeiouAEIOU")))
     (if is-vowel
         (+ 1 (count-vowels rest))
         (count-vowels rest)))))

Filtering characters:

(defun remove-spaces
  (("") "")
  (((cons #\space rest)) (remove-spaces rest))
  (((cons c rest)) (cons c (remove-spaces rest))))

Using list comprehensions on strings:

lfe> (lc ((<- c "hello")) (- c 32))
"HELLO"

lfe> (lc ((<- c "hello world") (=/= c #\space)) c)
"helloworld"

Performance Notes

String operations have the same performance characteristics as list operations because they are list operations:

  • Concatenation with ++ is O(n) in the length of the first string
  • Repeated concatenation in a loop can be O(n²)
  • For building large strings, accumulate in a list and call lists:flatten once
  • Consider using binaries for large text or when performance is critical

Example of efficient string building:

(defun build-csv-row (items)
  (lists:flatten
    (lists:join ","
      (lists:map #'format-item/1 items))))

Summary

Strings in LFE are lists of character codes—simple, elegant, and slightly peculiar if you're new to this representation. This design choice means:

  • All list operations work on strings
  • Strings naturally integrate with pattern matching
  • String manipulation is intuitive if you think in terms of list processing
  • Performance characteristics match those of lists
  • Unicode requires some care but is fully supported

For most string operations in modern code, prefer the string module's Unicode-aware functions. For performance-critical text processing with large strings, consider using binaries (covered in a later chapter). But for everyday string manipulation, list-based strings remain a perfectly reasonable and often elegant choice.

Remember: in LFE, as in life, it's not about having a separate type for everything—it's about recognizing that sometimes, a list of integers masquerading as text is exactly what you need.