#set document(title: "2.4 Types", author: "Modular Inc. / XYZ Homework") #set page(width: 8.5in, height: auto, margin: 1in) #import "@preview/cetz:0.5.2" #set text(font: ("STIX Two Text", "Libertinus Serif", "New Computer Modern"), size: 10.5pt, lang: "en") #show math.equation: set text(font: ("STIX Two Math", "New Computer Modern Math")) #set par(justify: true, leading: 0.62em, spacing: 0.9em) #set enum(spacing: 1.1em) // room between list items so tall inline fractions don't collide #set list(spacing: 1.1em) #set table(stroke: 0.5pt + rgb("#c7ccd3")) #let BLUE = rgb("#183B6F") // brand navy — section bars + example/solution labels (white on navy 11.09:1) #let ORANGE = rgb("#A94509") // brand primary-700 — AA-safe deep orange for TEXT (5.93:1 on white; raw brand #F37021 is 2.94:1 and must never carry text) #let RED = rgb("#DC2626") // brand error-600 #let GREEN = rgb("#059669") // brand success-600 (decoration only; small green text uses green-text #007942) #show heading.where(level: 1): it => block(width: 100%, above: 0pt, below: 16pt, fill: gradient.linear(BLUE, rgb("#2C5AA0")), inset: (x: 14pt, y: 12pt), radius: 3pt, text(fill: white, weight: "bold", size: 19pt, it.body)) #show heading.where(level: 2): it => block(width: 100%, above: 18pt, below: 10pt, fill: BLUE, inset: (x: 10pt, y: 6pt), radius: 2pt, text(fill: white, weight: "bold", size: 12pt, it.body)) #show heading.where(level: 3): it => text(fill: ORANGE, weight: "bold", size: 12.5pt, it.body) #show heading.where(level: 4): it => text(fill: BLUE, weight: "bold", size: 10.5pt, it.body) #let examplebox(label, title, body) = block(width: 100%, breakable: true, fill: rgb("#EFF1F5"), stroke: 0.5pt + rgb("#CFDDF0"), radius: 4pt, inset: 10pt, above: 12pt, below: 12pt)[ #block(below: 6pt)[#box(fill: BLUE, inset: (x: 6pt, y: 2pt), radius: 2pt, text(fill: white, weight: "bold", size: 8.5pt, label)) #h(0.4em) #strong[#title]] #body] // rail = decorative left rule (raw brand token); labelcolor = AA-safe label text shade #let notebox(label, rail, labelcolor, tint, body) = block(width: 100%, breakable: true, fill: tint, stroke: (left: 3pt + rail), inset: (left: 10pt, rest: 8pt), radius: (right: 4pt), above: 11pt, below: 11pt)[ #text(fill: labelcolor, weight: "bold", size: 7.5pt, tracking: 0.5pt)[#upper(label)] #linebreak() #body] #let solutionbox(body) = block(above: 4pt, below: 8pt)[ #text(fill: BLUE, weight: "bold", size: 8.5pt)[Solution] #linebreak() #body] #let figph(msg) = block(width: 100%, height: 60pt, fill: rgb("#f6f7f9"), stroke: (paint: rgb("#c7ccd3"), dash: "dashed"), radius: 4pt, inset: 10pt)[ #align(center + horizon, text(fill: rgb("#889"), style: "italic", size: 9pt, msg))] // Standardize inlined figure sizes: measure the natural CeTZ canvas, then scale to a // consistent envelope (aspect-aware; see build_typst.py FIG_* constants). Unlike the // print preamble, dimensions are FLOORED: in an editor a user can trim a figure to a // degenerate 1-D shape (a bare line), and w/h or tw/w would then divide by zero. #let _STD_W = 3.5 #let _WIDE_W = 5.6 #let _MAX_H = 3.4 #let _ASPECT_WIDE = 2.2 #let _UPSCALE_MAX = 1.15 #let stdfig(body) = context { let m = measure(body) let w = calc.max(m.width / 1in, 0.01) let h = calc.max(m.height / 1in, 0.01) let tw = if w / h > _ASPECT_WIDE { _WIDE_W } else { _STD_W } let s = calc.min(tw / w, _MAX_H / h, _UPSCALE_MAX) align(center, box(scale(x: s * 100%, y: s * 100%, reflow: true, body))) } #show figure: set block(breakable: false) #set figure(gap: 8pt) #show figure.caption: set text(size: 8.5pt, fill: rgb("#555")) == 2.4#h(0.6em)Types All values in Mojo have an associated data type. Most of the types are #emph[nominal] types, defined by a struct. These types are nominal (or "named") because type equality is determined by the type's #emph[name], not its #emph[structure]. There are some types that aren't defined as structs: - Functions are typed based on their signatures. - NoneType is a type with one instance, the None object, which is used to signal "no value." Mojo comes with a standard library that provides a number of useful types and utility functions. These standard types aren't privileged. Each of the standard library types is defined just like user-defined types—even basic types like #link("https://mojolang.org/docs/std/builtin/int/Int/")[Int] and #link("https://mojolang.org/docs/std/collections/string/string/String/")[String]. But these standard library types are the building blocks you'll use for most Mojo programs. The most common types are #emph[built-in types], which are always available and don't need to be imported. These include types for numeric values, strings, boolean values, and others. The standard library also includes many more types that you can import as needed, including collection types, utilities for interacting with the filesystem and getting system information, and so on. === Numeric types Mojo provides built-in numeric types that represent signed integers, unsigned integers, and floating-point values. These types support multiple precisions and are used to model both low-level data and high-level numeric computation. The following sections introduce integer and floating-point types in Mojo. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ All numeric types support the usual numeric and bitwise operators. The #link("https://mojolang.org/docs/std/math/")[math] module provides additional math functions. ] ==== Integers and unsigned integers Mojo supports both signed (Int) and unsigned (UInt) integer types. These include both fixed-size and general types: - If you need a fixed-size integer, Mojo provides explicit-width integer types such as Int8, Int16, UInt32, and UInt64. These fixed-precision integer types are aliases to the #link("https://mojolang.org/docs/std/builtin/simd/SIMD/")[SIMD] type. - Use the general Int or UInt types when you don't require a specific bit width. Int represents a signed integer that uses the system's native word size, typically 64 bits on 64-bit CPUs and 32 bits on 32-bit CPUs. Similarly, UInt represents an unsigned integer that uses the system's default word size. You may wonder when to use Int and when to use the other integer types. In general, Int is a good safe default when you need an integer type and you don't require a specific bit width. Using Int as the default integer type for APIs makes APIs more consistent and predictable. Signed versus unsigned Signed and unsigned integers with the same bit width can represent the same number of distinct values, but over different ranges. For example: - Int8 represents 256 values ranging from -128 to 127 - UInt8 represents 256 values ranging from 0 to 255 Overflow behavior Signed and unsigned integers differ in how they handle overflow. - When a signed integer overflows, the value wraps around into the negative range using two's complement arithmetic. For example, adding 1 to var si: Int8 = 127 results in -128. - When an unsigned integer overflows, the value wraps around to the beginning of its range. For example, adding 1 to var ui: UInt8 = 255 results in 0. You may prefer unsigned integers when negative values are not required, when you are not designing a public API, or when you want to maximize the usable positive range. Mojo-supported fixed-width integer types | Type name | Description | |-----------|------------------------| | Int8 | 8-bit signed integer | | Int16 | 16-bit signed integer | | Int32 | 32-bit signed integer | | Int64 | 64-bit signed integer | | Int128 | 128-bit signed integer | | Int256 | 256-bit signed integer | | Type name | Description | |-----------|--------------------------| | UInt8 | 8-bit unsigned integer | | UInt16 | 16-bit unsigned integer | | UInt32 | 32-bit unsigned integer | | UInt64 | 64-bit unsigned integer | | UInt128 | 128-bit unsigned integer | | UInt256 | 256-bit unsigned integer | ==== Floating-point numbers Mojo provides several floating-point types for representing real numbers at different precisions. Since floating-point values use a fixed number of bits, some numbers can't be represented exactly. The floating-point types Float64, Float32, and Float16 follow the IEEE 754-2008 standard for representing floating-point values. Each type includes a sign bit, a set of bits representing an exponent, and a set of bits representing the mantissa (also called fraction or significand). Table 3 shows how these types are represented in memory. | Type name | Sign | Exponent | Mantissa | |-----------|-------|----------|----------| | Float64 | 1 bit | 11 bits | 52 bits | | Float32 | 1 bit | 8 bits | 23 bits | | Float16 | 1 bit | 5 bits | 10 bits | Exponent values of all zeros or all ones represent special cases. These patterns allow floating-point numbers to encode positive and negative infinity, signed zeros, and not-a-number (NaN). These values are available as static constants provided by #link("https://mojolang.org/docs/std/builtin/float_literal/FloatLiteral/")[FloatLiteral]: from std.math import copysign from std.utils.numerics import isfinite, isinf, isnan var inf = FloatLiteral.infinity print(isinf(inf)) \# \`True\` print(inf \> 0) \# \`True\` var neginf = FloatLiteral.negative\_infinity print(isinf(neginf)) \# \`True\` print(neginf \< 0) \# \`True\` var nan = FloatLiteral.nan print(isnan(nan)) \# \`True\` var negzero = FloatLiteral.negative\_zero print(negzero == 0.0) \# \`True\` print(copysign(1.0, negzero) \< 0) \# \`True\` For more details on how floating-point numbers are represented, see #link("https://en.wikipedia.org/wiki/IEEE_754")[IEEE 754]. Floating-point approximations and comparisons Because floating-point values are approximate, they often cannot represent the exact mathematical value they are intended to model. - #strong[Rounding errors.] Rounding may produce unexpected results. For example, 1/3 cannot be represented exactly in floating-point formats. As more floating-point operations are performed, rounding errors may accumulate. - #strong[Space between consecutive numbers.] The distance between consecutive representable values varies across the range of a floating-point type. Near zero, values are densely packed. For large positive or negative numbers, the spacing can exceed 1, making it impossible to represent some consecutive integers. Because values are approximate, it is rarely useful to compare floating-point numbers using the equality operator (==). For example: var big\_num = 1.0e16 var bigger\_num = big\_num + 1.0 print(big\_num == bigger\_num) True Comparison operators (such as \< and \>=) work as expected with floating-point values. To test whether two values are equal within a tolerance, use the #link("https://mojolang.org/docs/std/math/math/isclose/")[math.isclose()] function to compare whether two floating-point numbers are equal within a specified tolerance. Mojo-supported floating-point types In the following table, the #strong[eXmX] format (for example, Float8\_e5m2 and Float8\_e4m3fn) refers to the number of bits allocated to a floating-point number's exponent and mantissa. All IEEE 754 floating-point formats use an implied leading 1. That means Float32 is e8m23 but effectively e8m24, Float16 is e5m10 but effectively e5m11, BFloat16 is e8m7 but effectively e8m8, and Float8\_e4m3fn is e4m3 but effectively e4m4. In addition to eXmX: - #strong[fn] signifies finite numbers only. The numbers are valid floating-point values that are not infinite. NaN is supported. - #strong[uz] means unsigned zero. Only +0 is supported, not -0. Although Mojo supports all these types, these types are not supported on all hardware. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ The #emph[B] in BFloat16 stands for Brain, from the Google Brain artificial intelligence research group. Google developed it specifically for their Tensor Processing Units (TPUs) to accelerate machine learning workloads. Therefore the B is a project identifier and not a technical format indicator. ] | Type name | Description | CPU/GPU Support | |-------------------|------------------------------------------------------------------------------------------------------------------------|-----------------| | Float16 | 16-bit floating-point #linebreak() (IEEE 754-2008 binary16) | CPU and GPU | | Float32 | 32-bit floating-point #linebreak() (IEEE 754-2008 binary32) | CPU and GPU | | Float64 | 64-bit floating-point #linebreak() (IEEE 754-2008 binary64) | CPU and GPU | | BFloat16 | 16-bit floating-point #linebreak() (16-bit version of IEEE 754 binary32) | CPU and GPU | | Float4\_e2m1fn | 4-bit floating-point #linebreak() (e2m1 format from Open Compute MX specification — finite values and NaN only, no infinities) | GPU | | Float8\_e5m2 | 8-bit floating-point #linebreak() (OFP8 e5m2 format) | GPU | | Float8\_e5m2fnuz | 8-bit floating-point #linebreak() (AMD-only e5m2fnuz format — finite values and NaN only, no infinities) | GPU | | Float8\_e4m3fn | 8-bit floating-point #linebreak() (OFP8 e4m3fn format — finite values and NaN only, no infinities) | GPU | | Float8\_e4m3fnuz | 8-bit floating-point #linebreak() (AMD-only e4m3fnuz format — finite values and NaN only, no infinities) | GPU | #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ GPU-only floating-point types are supported on specific accelerator hardware and may not be available on all GPUs. ] AI-optimized floating-point formats Several floating-point types are specifically designed for AI and machine learning workloads, trading precision for memory efficiency and computational throughput. #strong[BFloat16 (Brain Floating Point)] uses the same 8 exponent bits as Float32, preserving its dynamic range, but uses 7 explicit bits (8 effective bits) for the mantissa compared to Float32's 23 bits. This makes it ideal for neural network training where gradient magnitudes vary widely but high precision is less critical. #strong[8-bit formats] (Float8\_e5m2, Float8\_e4m3fn, and their fnuz variants) are ultra-compact formats for AI accelerators where memory bandwidth is the primary bottleneck. The naming indicates bit allocation: e5m2 means 5 exponent bits and 2 mantissa bits. The fnuz suffix additionally denotes unsigned zero (no -0), used in AMD hardware. #strong[4-bit format] (Float4\_e2m1fn) offers extreme compression with only 2 exponent bits and 1 mantissa bit, used in specialized inference scenarios where accuracy can be traded for maximum throughput. ==== Numeric literals In addition to these numeric types, the standard libraries provides integer and floating-point literal types, #link("https://mojolang.org/docs/std/builtin/int_literal/IntLiteral/")[IntLiteral] and #link("https://mojolang.org/docs/std/builtin/float_literal/FloatLiteral/")[FloatLiteral]. These literal types are used at compile time to represent literal numbers that appear in the code. In general, you should never instantiate these types yourself. Table 5 summarizes the literal formats you can use to represent numbers. | Format | Examples | Notes | |------------------------|-----------------|--------------------------------------------------------------------------------------------------| | Integer literal | 1760 | Integer literal, in decimal format. | | Hexadecimal literal | 0xaa, 0xFF | Integer literal, in hexadecimal format. #linebreak() Hex digits are case-insensitive. | | Octal literal | 0o77 | Integer literal, in octal format. | | Binary literal | 0b0111 | Integer literal, in binary format. | | Floating-point literal | 3.14, 1.2e9 | Floating-point literal. #linebreak() Must include the decimal point to be interpreted as floating-point. | At compile time, the literal types are arbitrary-precision (also called infinite-precision) values, so the compiler can perform compile-time calculations without overflow or rounding errors. At runtime the values are converted to finite-precision types—Int for integer values, and Float64 for floating-point values. (This process of converting a value that can only exist at compile time into a runtime value is called #emph[materialization].) The following code sample shows the difference between an arbitrary-precision calculation and the same calculation done using Float64 values at runtime, which suffers from rounding errors. var arbitrary\_precision = 3.0 \* (4.0 / 3.0 - 1.0) \# use a variable to force the following calculation to occur at runtime var three = 3.0 var finite\_precision = three \* (4.0 / three - 1.0) print(arbitrary\_precision, finite\_precision) 1.0 0.99999999999999978 ==== SIMD and DType To support high-performance numeric processing, Mojo uses the #link("https://mojolang.org/docs/std/builtin/simd/SIMD/")[SIMD] type as the basis for its numeric types. SIMD (single instruction, multiple data) is a processor technology that allows you to perform an operation on an entire set of operands at once. Mojo's SIMD type abstracts SIMD operations. A SIMD value represents a SIMD #emph[vector]—that is, a fixed-size array of values that can fit into a processor's register. SIMD vectors are defined by two #emph[parameters]: - A DType value, defining the data type in the vector (for example, 32-bit floating-point numbers). - The number of elements in the vector, which must be a power of two. For example, you can define a vector of four Float32 values like this: var vec = SIMD\[DType.float32, 4\](3.0, 2.0, 2.0, 1.0) Math operations on SIMD values are applied #emph[elementwise], on each individual element in the vector. For example: var vec1 = SIMD\[DType.int8, 4\](2, 3, 5, 7) var vec2 = SIMD\[DType.int8, 4\](1, 2, 3, 4) var product = vec1 \* vec2 print(product) \[2, 6, 15, 28\] ==== Scalar values The SIMD module defines several comptime values that function as #emph[type aliases]—shorthand names for different types of SIMD vectors. In particular, the Scalar type is just a SIMD vector with a single element. The numeric types listed in Table 1, like Int8 and Float32 are actually type aliases for different types of scalar values: comptime Scalar = SIMD\[size=1\] comptime Int8 = Scalar\[DType.int8\] comptime Float32 = Scalar\[DType.float32\] This may seem a little confusing at first, but it means that whether you're working with a single Float32 value or a vector of float32 values, the math operations go through exactly the same code path. The DType type The DType struct describes the different data types that a SIMD vector can hold, and defines a number of utility functions for operating on those data types. The DType struct defines a set of comptime members that act as identifiers for the different data types, like DType.int8 and DType.float32. You use these comptime members when declaring a SIMD vector: var v: SIMD\[DType.float64, 16\] Note that DType.float64 isn't a #emph[type], it's a value that describes a data type. You can't create a variable with the type DType.float64. You can create a variable with the type SIMD\[DType.float64, 1\] (or Float64, which is the same thing). from std.utils.numerics import max\_finite, min\_finite def describeDType\[dtype: DType\](): print(dtype, "is floating-point:", dtype.is\_floating\_point()) print(dtype, "is integral:", dtype.is\_integral()) print("Min/max finite values for", dtype) print(min\_finite\[dtype\](), max\_finite\[dtype\]()) describeDType\[DType.float32\]() float32 is floating-point: True float32 is integral: False Min/max finite values for float32 -3.4028234663852886e+38 3.4028234663852886e+38 There are several other data types in the standard library that also use the DType abstraction. ==== Numeric type conversion Constructors and implicit conversion documents the circumstances in which Mojo automatically converts a value from one type to another. Importantly, numeric operators #strong[don't] automatically narrow or widen operands to a common type. You can explicitly convert a SIMD value to a different SIMD type either by invoking its #link("https://mojolang.org/docs/std/builtin/simd/SIMD/#cast")[cast()] method or by passing it as an argument to the constructor of the target type. For example: simd1 = SIMD\[DType.float32, 4\](2.2, 3.3, 4.4, 5.5) simd2 = SIMD\[DType.int16, 4\](-1, 2, -3, 4) simd3 = simd1 \* simd2.cast\[DType.float32\]() \# Convert with cast() method print("simd3:", simd3) simd4 = simd2 + SIMD\[DType.int16, 4\](simd1) \# Convert with SIMD constructor print("simd4:", simd4) simd3: \[-2.2, 6.6, -13.200001, 22.0\] simd4: \[1, 5, 1, 9\] You can convert a Scalar value by passing it as an argument to the constructor of the target type. For example: var my\_int: Int16 = 12 \# SIMD\[DType.int16, 1\] var my\_float: Float32 = 0.75 \# SIMD\[DType.float32, 1\] result = Float32(my\_int) \* my\_float \# Result is SIMD\[DType.float32, 1\] print("Result:", result) Result: 9.0 You can convert a scalar value of any numeric type to Int by passing the value to the #link("https://mojolang.org/docs/std/builtin/int/Int/#__init__")[Int()] constructor method. Additionally, you can pass an instance of any struct that implements the #link("https://mojolang.org/docs/std/builtin/int/Intable/")[Intable] trait or #link("https://mojolang.org/docs/std/builtin/int/IntableRaising/")[IntableRaising] trait to the Int() constructor to convert that instance to an Int. You can convert an Int or IntLiteral value to the UInt type by passing the value to the #link("https://mojolang.org/docs/std/builtin/simd/#uint")[UInt()] constructor. You can't convert other numeric types to UInt directly, though you can first convert them to Int and then to UInt. === Strings Strings are Mojo's primary text type. They store UTF-8 encoded text and provide a safe, ergonomic interface for string manipulation. Mojo's String type is a mutable string. String supports a variety of operators and common methods: var s: String = "Testing" s += " Mojo strings" print(s) \# Testing Mojo strings ==== Construction Many standard library types conform to the #link("https://mojolang.org/docs/std/format/Writable/")[Writable] trait, which indicates that a value can be converted into a String using the String(...) constructor. The built-in #link("https://mojolang.org/docs/std/io/io/print/")[print()] function accepts values that conform to the Writable trait. Use String(value) to explicitly convert a value to a String: var s = "Items in list: " + String(5) print(s) \# Items in list: 5 Or, use the string constructor with variadic Writable types, so you don't have to call String() on each value: var s = String("Items in list: ", 5) print(s) \# Items in list: 5 ==== Emoji and grapheme clusters Mojo source files are UTF-8, letting you write emoji and other non-ASCII characters directly inside string literals. var wave = "👋" The standard library counts emoji three different ways, and the answers usually disagree: - byte\_length() returns the number of UTF-8 bytes. - count\_codepoints() counts the Unicode code points. - count\_graphemes() returns the number of user-perceived characters (grapheme clusters), following #link("https://www.unicode.org/reports/tr29/")[UAX \#29]. The grapheme count is what matches what a human would tell you if you asked them to "count characters." A family emoji, a flag, and a waving hand with a skin tone are each one grapheme, even though each is built from several joined code points: def show(label: StaticString, s: StringSlice): print( label, "bytes=", s.byte\_length(), "codepoints=", s.count\_codepoints(), "graphemes=", s.count\_graphemes(), ) def main(): show("family ", "👨‍👩‍👧‍👦") show("flag ", "🇺🇸") show("wave ", "👋🏽") show("namaste ", "नमस्ते") family bytes= 25 codepoints= 7 graphemes= 1 flag bytes= 8 codepoints= 2 graphemes= 1 wave bytes= 8 codepoints= 2 graphemes= 1 namaste bytes= 18 codepoints= 6 graphemes= 3 Output: family bytes=25 codepoints=7 graphemes=1 flag bytes=8 codepoints=2 graphemes=1 wave bytes=8 codepoints=2 graphemes=1 namaste bytes=18 codepoints=6 graphemes=3 If you'd rather not embed non-ASCII bytes in your source, for example, in ASCII-only codebases, you can spell the code point with a hex escape or chr(): var wave = "\\U0001F44B" \# 8-digit hex escape var wave2 = chr(0x1F44B) \# chr() function var copy = "\\u00A9" \# 4-digit hex escape, © var euro = "\\u20AC" \# 4-digit hex escape, € Mojo's 4-digit \\uHHHH escape spells code points up to U+FFFF. The 8-digit \\U form extends to U+10FFFF, Unicode's upper limit, covering emoji like 👋 (U+1F44B) and other characters above the Basic Multilingual Plane. The chr() function works for the full range too. Both forms reject surrogate code points (U+D800 to U+DFFF). Surrogates are reserved for UTF-16 encoding and aren't valid on their own. To escape a character above U+FFFF, write its full code point with \\U. Don't use a UTF-16 surrogate pair. ==== String formatting The format() method inserts values into a string using manual or automatic positional indexing. Replacement fields use braces: print("{0} {1} {0}".format("Mojo", 1.125)) \# Mojo 1.125 Mojo print("{} {}".format(True, "hello world")) \# True hello world Mojo's TString (template string) replaces the functionality of the format() method with direct expressions and better performance characteristics. TStrings work like format(), but they insert #link("https://mojolang.org/docs/std/format/Writable/")[Writable] representations of expressions into replacement fields. This provides safe and flexible string processing: var count = 3 var items = "apples" var template = t"Give me {count} {items}." \# Template string print(template) \# Output: Give me 3 apples. TString values are lazy. They don't allocate until you explicitly construct a String: var x = 41 print(t"The answer is {x + 1}") \# The answer is 42 (no allocation) var name = "Nate" var template = t"Hello, {name}!" \# template creation print(template) \# Hello, Nate! (no allocation) var s = String(template) \# explicitly construct a string TStrings can add arbitrary expressions within the replacement fields: var list = \[1, 2, 3\] print(t"{list\[0\] + list\[1\]}") \# 3 ==== Raw strings In Mojo, #strong[raw strings] are string literals prefixed with r. If you ran the following command it would print on one line, not two, because raw strings prevent backslash escape sequences from being interpreted: print(r"Hello\\nWorld") \# Hello\\nWorld, with the backslash and n Raw strings help with regular expressions, code generation, serialization code, and other applications where you want to use escape sequences as literal entries in your string. Unlike normal strings, escapes aren't processed. Rawness applies to all forms of strings: single line, multi-line, docstrings, and TStrings. Raw TStrings support interpolation but won't expand escape sequences: var name = "Nate" print(rt"Hello,\\t{name}.") \# Hello,\\tNate., with the backslash and t Raw strings #emph[still] need a way to terminate, so if you have to use " within your content, use an alternate form of quote, such as single quote (') or triple quotes, (""", ''') to enclose it: r'She said, "Hello, World!"' ==== String literals As with numeric types, the standard library includes a string literal type used to represent literal strings in the program source. String literals are enclosed in either single or double quotes. Adjacent literals are concatenated together, so you can define a long string using a series of literals broken up over several lines: comptime s = "A very long string which is " "broken into two literals for legibility." To define a multi-line string, enclose the literal in three single or double quotes: comptime s = """ Multi-line string literals let you enter long blocks of text, including newlines.""" Note that the triple double quote form is also used for API documentation strings. A StringLiteral will materialize to a String when used at run-time: comptime param = "foo" \# type = StringLiteral var runtime\_value = "bar" \# type = String var runtime\_value2 = param \# type = String === Booleans Mojo's Bool type represents a boolean value. It can take one of two values, True or False. You can negate a boolean value using the not operator. var conditionA = False var conditionB: Bool conditionB = not conditionA print(conditionA, conditionB) False True Many types have a boolean representation. Any type that implements the #link("https://mojolang.org/docs/std/builtin/bool/Boolable/")[Boolable] trait has a boolean representation. As a general principle, collections evaluate as True if they contain any elements, False if they are empty; strings evaluate as True if they have a non-zero length. === Tuples Mojo's Tuple type represents an immutable tuple consisting of zero or more values, separated by commas. Tuples can consist of multiple types and you can index into tuples in multiple ways. \# Tuples are immutable and can hold multiple types example\_tuple = Tuple\[Int, String\](1, "Example") \# Assign multiple variables at once x, y = example\_tuple print(x, y) \# Get individual values with an index s = example\_tuple\[1\] print(s) 1 Example Example You can also create a tuple without explicit typing. example\_tuple = (1, "Example") s = example\_tuple\[1\] print(s) Example === Collection types The Mojo standard library also includes a set of basic collection types that can be used to build more complex data structures: - #link("https://mojolang.org/docs/std/collections/list/List/")[List], a dynamically-sized array of items. - #link("https://mojolang.org/docs/std/collections/dict/Dict/")[Dict], an associative array of key-value pairs. - #link("https://mojolang.org/docs/std/collections/set/Set/")[Set], an unordered collection of unique items. - #link("https://mojolang.org/docs/std/collections/optional/Optional/")[Optional] represents a value that may or may not be present. The collection types are #emph[generic types]: while a given collection can only hold a specific type of value (such as Int or Float64), you specify the type at compile time using a parameter. For example, you can create a List of Int values like this: var l: List\[Int\] = \[1, 2, 3, 4\] \# l.append(3.14) \# error: FloatLiteral cannot be converted to Int You don't always need to specify the type explicitly. If Mojo can #emph[infer] the type, you can omit it. For example, when you construct a list from a set of integer literals, Mojo creates a List\[Int\]. \# Inferred type == List\[Int\] var l1 = \[1, 2, 3, 4\] Where you need a more flexible collection, the #link("https://mojolang.org/docs/std/utils/variant/Variant/")[Variant] type can hold different types of values. For example, a Variant\[Int32, Float64\] can hold either an Int32 #emph[or] a Float64 value at any given time. (Using Variant is not covered in this section, see the #link("https://mojolang.org/docs/std/utils/variant/Variant/")[API docs] for more information.) The following sections give brief introduction to the main collection types. ==== List #link("https://mojolang.org/docs/std/collections/list/List/")[List] is a dynamically-sized array of elements. List elements need to conform to the #link("https://mojolang.org/docs/std/builtin/value/Copyable/")[Copyable] trait. Most of the common standard library primitives, like Int, String, and SIMD conform to this trait. You can create a List by passing the element type as a parameter, like this: var l = List\[String\]() The List type supports a subset of the Python list API, including the ability to append to the list, pop items out of the list, and access list items using subscript notation. var list = \[2, 3, 5\] list.append(7) list.append(11) print("Popping last item from list: ", list.pop()) for idx in range(len(list)): print(list\[idx\], end=", ") Popping last item from list: 11 2, 3, 5, 7, Note that the previous code sample leaves out the type parameter when creating the list. Because the list is being created with a set of Int values, Mojo can #emph[infer] the type from the arguments. - Mojo supports list, set, and dictionary literals for collection initialization: \# List literal, element type infers to Int. var nums = \[2, 3, 5\] You can also use an explicit type if you want a specific element type: var list : List\[UInt8\] = \[2, 3, 5\] You can also use list "comprehensions" for compact conditional initialization: var list2 = \[x\*Int(y) for x in nums for y in list if x != 3\] - You can't print() a list, or convert it directly into a string. \# Does not work print(list) As shown above, you can print the individual elements in a list as long as they're a #link("https://mojolang.org/docs/std/format/Writable/")[Writable] type. - Iterating a List returns an immutable reference to each item: var list = \[2, 3, 4\] for item in list: print(item, end=", ") 2, 3, 4, If you would like to mutate the elements of the list, capture the reference to the element with ref instead of making a copy: var list = \[2, 3, 4\] for ref item in list: \# Capture a ref to the list element print(item, end=", ") item = 0 \# Mutates the element inside the list print("\\nAfter loop:", list\[0\], list\[1\], list\[2\]) 2, 3, 4, After loop: 0 0 0 You can see that the original loop entries were modified. ==== Dict The #link("https://mojolang.org/docs/std/collections/dict/Dict/")[Dict] type is an associative array that holds key-value pairs. You can create a Dict by specifying the key type and value type as parameters and using dictionary literals: \# Empty dictionary var empty\_dict: Dict\[String, Float64\] = {} \# Dictionary with initial key-value pairs var values: Dict\[String, Float64\] = {"pi": 3.14159, "e": 2.71828} You can also use the constructor syntax: var values = Dict\[String, Float64\]() The dictionary's key type must conform to the #link("https://mojolang.org/docs/std/collections/dict/#keyelement")[KeyElement] trait, and value elements must conform to the #link("https://mojolang.org/docs/std/builtin/value/Copyable/")[Copyable] trait. You can insert and remove key-value pairs, update the value assigned to a key, and iterate through keys, values, or items in the dictionary. The Dict iterators all yield references, which are copied into the declared name by default, but you can use the ref marker to avoid the copy: var d: Dict\[String, Float64\] = { "plasticity": 3.1, "elasticity": 1.3, "electricity": 9.7 } for item in d.items(): print(item.key, item.value) plasticity 3.1000000000000001 elasticity 1.3 electricity 9.6999999999999993 This is an unmeasurable micro-optimization in this case, but is useful when working with types that aren't Copyable. ==== Set The #link("https://mojolang.org/docs/std/collections/set/Set/")[Set] type represents a set of unique values. You can add and remove elements from the set, test whether a value exists in the set, and perform set algebra operations, like unions and intersections between two sets. Sets are generic and the element type must conform to the #link("https://mojolang.org/docs/std/collections/dict/#keyelement")[KeyElement] trait. Like lists and dictionaries, sets support standard literal syntax, as well as generator comprehensions: i\_like = {"sushi", "ice cream", "tacos", "pho"} you\_like = {"burgers", "tacos", "salad", "ice cream"} we\_like = i\_like.intersection(you\_like) print("We both like:") for item in we\_like: print("-", item) We both like: - ice cream - tacos ==== Optional An #link("https://mojolang.org/docs/std/collections/optional/Optional/")[Optional] represents a value that may or may not be present. Like the other collection types, it is generic, and can hold any type that conforms to the #link("https://mojolang.org/docs/std/builtin/value/Copyable/")[Copyable] trait. \# Two ways to initialize an Optional with a value var opt1 = Optional(5) var opt2: Optional\[Int\] = 5 \# Two ways to initialize an Optional with no value var opt3 = Optional\[Int\]() var opt4: Optional\[Int\] = None An Optional evaluates as True when it holds a value, False otherwise. If the Optional holds a value, you can retrieve a reference to the value using the value() method. But calling value() on an Optional with no value results in undefined behavior, so you should always guard a call to value() inside a conditional that checks whether a value exists. var opt: Optional\[String\] = "Testing" if opt: var value\_ref = opt.value() print(value\_ref) Testing Alternately, you can use the or\_else() method, which returns the stored value if there is one, or a user-specified default value otherwise: var custom\_greeting: Optional\[String\] = None print(custom\_greeting.or\_else("Hello")) custom\_greeting = "Hi" print(custom\_greeting.or\_else("Hello")) Hello Hi