Writing CSV
All writes start with writer (), which returns a Config with the same defaults as reader (). Configure it, then call write or write_keyed. The result is a single String (a BEAM binary) you can pipe to File.write! or send over the wire.
Positional rows with write
let csv =
writer ()
|> write [["a", "b"], ["c", "d"]]
# "a,b\nc,d\n"Every row terminates with \n, including the last one. Fields are written verbatim unless they need quoting (see below).
Adding a header row
with_headers prepends a row to the output. It does not affect how write interprets the data — it just adds a row up front:
let csv =
writer ()
|> with_headers ["name", "age"]
|> write [["Alice", "30"], ["Bob", "25"]]
# "name,age\nAlice,30\nBob,25\n"If you call with_headers with [] (the default), no header row is added. Use this when the consumer will supply its own column names.
Keyed rows with write_keyed
When your data is List (Dict String String), use write_keyed plus with_headers to project the dicts into a fixed column order:
let alice = Dict.new () |> Dict.put "name" "Alice" |> Dict.put "age" "30"
let csv =
writer ()
|> with_headers ["name", "age"]
|> write_keyed [alice]
# "name,age\nAlice,30\n"with_headers is mandatory here — write_keyed panics if you forget it, because there is no other way to know the column order. Keys missing from a dict are written as empty strings; keys present in the dict but not in with_headers are silently dropped.
Custom separators
writer ()
|> with_separator "\t"
|> write [["a", "b"], ["c", "d"]]
# "a\tb\nc\td\n"Same one-byte ASCII constraint as on the read side.
What gets quoted
csv-core quotes the minimum necessary. A field is wrapped in "..." when it contains the delimiter, a ", or a newline. Embedded quotes are doubled:
writer () |> write [["hello, world", "ok"]]
# "\"hello, world\",ok\n"
writer () |> write [["say \"hi\"", "ok"]]
# "\"say \"\"hi\"\"\",ok\n"
writer () |> write [["line1\nline2", "ok"]]
# "\"line1\nline2\",ok\n"You never need to pre-escape fields yourself — doing so will produce double-escaped output.
Round-tripping
write followed by parse (with skip_header if you used with_headers) returns the original data unchanged:
let data = [["Alice", "30"], ["Bob", "25"]]
let csv =
writer ()
|> with_headers ["name", "age"]
|> write data
let rows =
reader ()
|> skip_header
|> parse csv
# rows == dataThe same holds for write_keyed + parse_keyed — see the round-trip tests in tests/csv_test.dy for the full picture.