├── .github ├── FUNDING.yml └── workflows │ └── docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── additional ├── ferris.css └── ferris.js ├── book.toml ├── code ├── ch01-01-building-an-intuition │ ├── cell-counterexample │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── cell-example │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── cell-lengthener │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── cell-shortener │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── fn-ptr-lengthener │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── lifetime-lengthener │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── lifetime-shortener │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs ├── ch01-02-formalizing-variance │ ├── outlives-example │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── quick-exercise │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs ├── ch01-03-conflicts-and-type-parameters │ ├── lifetime-check │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── two-spots │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs └── ch01-04-variance-in-practice │ ├── message-displayer-and-collector │ ├── Cargo.toml │ └── src │ │ └── main.rs │ ├── overview │ ├── Cargo.toml │ └── src │ │ └── main.rs │ ├── simple-message-collector │ ├── Cargo.toml │ └── src │ │ └── main.rs │ └── simple-message-displayer │ ├── Cargo.toml │ └── src │ └── main.rs ├── src ├── SUMMARY.md ├── ch01-01-building-an-intuition.md ├── ch01-02-formalizing-variance.md ├── ch01-03-conflicts-and-type-parameters.md ├── ch01-04-variance-in-practice.md ├── ch01-05-epilogue.md ├── ch02-00-acknowledgements.md └── img │ └── does_not_compile.svg └── theme └── head.hbs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: sunshowers 2 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: Docs 7 | 8 | jobs: 9 | docs: 10 | concurrency: ci-${{ github.ref }} 11 | name: Build and deploy documentation 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | toolchain: stable 18 | - name: Install static-sitemap-cli 19 | run: npm install static-sitemap-cli 20 | - name: Install mdbook 21 | uses: baptiste0928/cargo-install@v1 22 | with: 23 | crate: mdbook 24 | version: 0.4 25 | - name: Build book 26 | run: | 27 | mdbook build 28 | - name: Generate sitemap 29 | run: | 30 | cd book 31 | npx sscli --base https://lifetime-variance.sunshowers.io 32 | - name: Deploy 33 | uses: JamesIves/github-pages-deploy-action@releases/v4 34 | with: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | branch: gh-pages 37 | folder: book 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | book/ 4 | /index.html 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lifetime variance in Rust 2 | 3 | This document covers the basics of *variance* in Rust, as it applies to lifetimes, using code examples. 4 | 5 | ## Locations 6 | 7 | This document is hosted online at [https://lifetime-variance.sunshowers.io](https://lifetime-variance.sunshowers.io). The source is hosted [on GitHub](https://github.com/sunshowers-code/lifetime-variance). 8 | 9 | This document is available offline by installing `git` and running the following command while online. 10 | 11 | ``` 12 | git clone https://github.com/sunshowers-code/lifetime-variance --branch gh-pages 13 | ``` 14 | 15 | then pointing your web browser at `lifetime-variance/index.html`. 16 | 17 | [Pull requests](https://github.com/sunshowers-code/lifetime-variance/compare) to fix typos or unclear language are welcome! 18 | 19 | ## License 20 | 21 | [CC0](https://creativecommons.org/publicdomain/zero/1.0/) 22 | -------------------------------------------------------------------------------- /additional/ferris.css: -------------------------------------------------------------------------------- 1 | body.light .does_not_compile, 2 | body.rust .does_not_compile { 3 | background: #fff1f1; 4 | } 5 | 6 | body.coal .does_not_compile, 7 | body.navy .does_not_compile, 8 | body.ayu .does_not_compile { 9 | background: #501f21; 10 | } 11 | 12 | .ferris { 13 | position: absolute; 14 | z-index: 99; 15 | right: 5px; 16 | top: 30px; 17 | width: 10%; 18 | height: auto; 19 | } 20 | 21 | .ferris-explain { 22 | width: 100px; 23 | } 24 | -------------------------------------------------------------------------------- /additional/ferris.js: -------------------------------------------------------------------------------- 1 | const ferrisTypes = [ 2 | { 3 | attr: 'does_not_compile', 4 | title: 'This code does not compile!' 5 | } 6 | ] 7 | 8 | document.addEventListener('DOMContentLoaded', () => { 9 | for (const ferrisType of ferrisTypes) { 10 | attachFerrises(ferrisType) 11 | } 12 | }) 13 | 14 | function attachFerrises(type) { 15 | const elements = document.getElementsByClassName(type.attr) 16 | 17 | for (const codeBlock of elements) { 18 | const lines = codeBlock.textContent.split(/\r|\r\n|\n/).length - 1; 19 | 20 | if (lines >= 4) { 21 | attachFerris(codeBlock, type) 22 | } 23 | } 24 | } 25 | 26 | function attachFerris(element, type) { 27 | const img = document.createElement('img') 28 | img.setAttribute('src', 'img/' + type.attr + '.svg') 29 | img.setAttribute('title', type.title) 30 | img.className = 'ferris' 31 | 32 | element.parentElement.insertBefore(img, element) 33 | } 34 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "Lifetime Variance Example" 3 | authors = ["Rain"] 4 | language = "en" 5 | multilingual = false 6 | src = "src" 7 | theme = "theme" 8 | description = "Rust tutorial for lifetime variance, with code examples to show how covariance and contravariance works." 9 | 10 | 11 | [output.html] 12 | git-repository-url = "https://github.com/sunshowers-code/lifetime-variance" 13 | edit-url-template = "https://github.com/sunshowers-code/lifetime-variance/edit/main/{path}" 14 | site-url = "https://lifetime-variance.sunshowers.io" 15 | additional-css = ["additional/ferris.css"] 16 | additional-js = ["additional/ferris.js"] 17 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/cell-counterexample/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cell-counterexample" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/cell-counterexample/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | use std::{collections::HashSet, iter::FromIterator}; 5 | 6 | fn hash_set_shortener<'a, 'b>(s: &'a mut HashSet<&'static str>) -> &'a mut HashSet<&'b str> { 7 | s 8 | } 9 | 10 | // ANCHOR: all 11 | fn hash_set_counterexample() { 12 | let mut my_set: HashSet<&'static str> = HashSet::from_iter(["static"]); 13 | let owned_string: String = "non_static".to_owned(); 14 | 15 | // If we pretend that hash_set_shortener works... 16 | let shorter_set = hash_set_shortener(&mut my_set); 17 | 18 | // then you could use `shorter_set` to insert a non-static string: 19 | shorter_set.insert(&owned_string); 20 | 21 | // Now we can drop `shorter_set` to regain the ability to use `my_set`: 22 | std::mem::drop(shorter_set); 23 | 24 | // And my_set now has a non-static string in it. Whoops! 25 | } 26 | // ANCHOR_END: all 27 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/cell-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cell-example" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/cell-example/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | use std::collections::HashSet; 5 | use std::iter::FromIterator; 6 | 7 | // ANCHOR: all 8 | fn hash_set_example() { 9 | // Consider this HashSet over static strings. 10 | let mut my_set: HashSet<&'static str> = HashSet::from_iter(["static"]); 11 | 12 | // Do you think this can work? 13 | let owned_string: String = "non_static".to_owned(); 14 | my_set.insert(&owned_string); 15 | 16 | // Doesn't seem like it can, right? my_set promises that the &strs inside it 17 | // are all 'static, but we tried to put in an owned string scoped to this 18 | // function. 19 | } 20 | // ANCHOR_END: all 21 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/cell-lengthener/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cell-lengthener" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/cell-lengthener/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | use std::collections::HashSet; 5 | 6 | // ANCHOR: all 7 | fn hash_set_lengthener<'a, 'b>( 8 | s: &'a mut HashSet<&'b str>, 9 | ) -> &'a mut HashSet<&'static str> { 10 | s 11 | } 12 | 13 | // ANCHOR_END: all 14 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/cell-shortener/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cell-shortener" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/cell-shortener/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | use std::collections::HashSet; 5 | 6 | // ANCHOR: all 7 | fn hash_set_shortener<'a, 'b>( 8 | s: &'a mut HashSet<&'static str>, 9 | ) -> &'a mut HashSet<&'b str> { 10 | s 11 | } 12 | 13 | // ANCHOR_END: all 14 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/fn-ptr-lengthener/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fn-pt-lengthener" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/fn-ptr-lengthener/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | // ANCHOR: all 5 | fn fn_ptr_lengthener<'a>(f: fn(&'a str) -> ()) -> fn(&'static str) -> () { 6 | f 7 | } 8 | // ANCHOR_END: all -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/lifetime-lengthener/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lifetime-lengthener" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/lifetime-lengthener/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | // ANCHOR: all 5 | fn lifetime_lengthener<'a>(s: &'a str) -> &'static str { 6 | s 7 | } 8 | 9 | // ANCHOR_END: all -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/lifetime-shortener/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lifetime-shortener" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-01-building-an-intuition/lifetime-shortener/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | // ANCHOR: all 5 | fn lifetime_shortener<'a>(s: &'static str) -> &'a str { 6 | s 7 | } 8 | // ANCHOR_END: all -------------------------------------------------------------------------------- /code/ch01-02-formalizing-variance/outlives-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "outlives-example" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] -------------------------------------------------------------------------------- /code/ch01-02-formalizing-variance/outlives-example/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() { } 3 | 4 | // ANCHOR: all 5 | struct OutlivesExample<'a, 'b: 'a> { 6 | a_str: &'a str, 7 | b_str: &'b str, 8 | } 9 | // ANCHOR_END: all 10 | -------------------------------------------------------------------------------- /code/ch01-02-formalizing-variance/quick-exercise/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quick-exercise" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-02-formalizing-variance/quick-exercise/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | use std::cell::Cell; 5 | 6 | // ANCHOR: Multi 7 | struct Multi<'a, 'b, 'c, 'd1, 'd2> { 8 | a: &'a str, 9 | b: Cell<&'b str>, 10 | c: fn(&'c str) -> usize, 11 | d: &'d1 mut &'d2 str, 12 | } 13 | // ANCHOR_END: Multi 14 | 15 | // ANCHOR: a 16 | fn a<'a, 'b, 'c, 'd1, 'd2>( 17 | x: Multi<'static, 'b, 'c, 'd1, 'd2> 18 | ) -> Multi<'a, 'b, 'c, 'd1, 'd2> { 19 | x 20 | } 21 | // ANCHOR_END: a 22 | 23 | // ANCHOR: c 24 | fn c<'a, 'b, 'c, 'd1, 'd2>( 25 | x: Multi<'a, 'b, 'c, 'd1, 'd2> 26 | ) -> Multi<'a, 'b, 'static, 'd1, 'd2> { 27 | x 28 | } 29 | // ANCHOR_END: c 30 | 31 | // ANCHOR: d1 32 | fn d1<'a, 'b, 'c, 'd1, 'd2>( 33 | x: Multi<'a, 'b, 'c, 'static, 'd2> 34 | ) -> Multi<'a, 'b, 'c, 'd1, 'd2> { 35 | x 36 | } 37 | // ANCHOR_END: d1 38 | -------------------------------------------------------------------------------- /code/ch01-03-conflicts-and-type-parameters/lifetime-check/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lifetime-check" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-03-conflicts-and-type-parameters/lifetime-check/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | // ANCHOR: TypeParams 5 | struct TypeParams { 6 | t: Vec, 7 | u: fn(U) -> (), 8 | } 9 | // ANCHOR_END: TypeParams 10 | 11 | // ANCHOR: LifetimeParams 12 | struct LifetimeParams<'a, 'b> { 13 | nested: TypeParams<&'a str, &'b str>, 14 | } 15 | // ANCHOR_END: LifetimeParams 16 | 17 | // ANCHOR: lifetime_check 18 | fn lifetime_check<'a, 'b>( 19 | x: LifetimeParams<'static, 'b> 20 | ) -> LifetimeParams<'a, 'static> { 21 | x 22 | } 23 | // ANCHOR_END: lifetime_check 24 | -------------------------------------------------------------------------------- /code/ch01-03-conflicts-and-type-parameters/two-spots/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "two-spots" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-03-conflicts-and-type-parameters/two-spots/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | use std::cell::Cell; 5 | 6 | // ANCHOR: all 7 | struct TwoSpots<'a> { 8 | foo: &'a str, 9 | bar: Cell<&'a str>, 10 | } 11 | // ANCHOR_END: all 12 | -------------------------------------------------------------------------------- /code/ch01-04-variance-in-practice/message-displayer-and-collector/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "message-displayer-and-collector" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-04-variance-in-practice/message-displayer-and-collector/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | use std::collections::HashSet; 5 | use std::fmt; 6 | 7 | // ANCHOR: all 8 | // Consider this struct representing a message. 9 | struct Message<'msg> { 10 | message: &'msg str, 11 | } 12 | 13 | // ... this struct that collects messages to be displayed. 14 | struct MessageCollector<'a, 'msg> { 15 | list: &'a mut Vec>, 16 | } 17 | 18 | impl<'a, 'msg> MessageCollector<'a, 'msg> { 19 | // This adds a message to the end of the list. 20 | fn add_message(&mut self, message: Message<'msg>) { 21 | self.list.push(message); 22 | } 23 | } 24 | 25 | // And this struct that displays collected messages. 26 | struct MessageDisplayer<'a, 'msg> { 27 | list: &'a Vec>, 28 | } 29 | 30 | impl<'a, 'msg> fmt::Display for MessageDisplayer<'a, 'msg> { 31 | // This displays all the messages, separated by newlines. 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | for message in self.list { 34 | write!(f, "{}\n", message.message)?; 35 | } 36 | Ok(()) 37 | } 38 | } 39 | 40 | fn message_example() { 41 | // Here's a simple pool of messages. 42 | let mut message_pool: HashSet = HashSet::new(); 43 | message_pool.insert("ten".to_owned()); 44 | message_pool.insert("twenty".to_owned()); 45 | 46 | // All right, let's try collecting and displaying some messages! 47 | collect_and_display(&message_pool); 48 | } 49 | 50 | fn collect_and_display<'msg>(message_pool: &'msg HashSet) { 51 | let mut list = vec![]; 52 | 53 | // Collect some messages. (This is pretty simple but you can imagine the 54 | // collector being passed into other code.) 55 | let mut collector = MessageCollector { list: &mut list }; 56 | for message in message_pool { 57 | collector.add_message(Message { message }); 58 | } 59 | 60 | // Now let's display those messages! 61 | let displayer = MessageDisplayer { list: &list }; 62 | println!("{}", displayer); 63 | } 64 | // ANCHOR_END: all 65 | -------------------------------------------------------------------------------- /code/ch01-04-variance-in-practice/overview/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "overview" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-04-variance-in-practice/overview/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | struct Message<'msg> { 5 | message: &'msg str, 6 | } 7 | 8 | // ANCHOR: MessageDisplayer 9 | struct MessageDisplayer<'a, 'msg> { 10 | // Two lifetime parameters: 11 | list: &'a Vec>, 12 | // Here, the compiler can vary the two independently, so the list can be 13 | // held onto a shorter lifetime than 'msg, then released. 14 | } 15 | // ANCHOR_END: MessageDisplayer 16 | 17 | // ANCHOR: SimpleMessageDisplayer 18 | struct SimpleMessageDisplayer<'a> { 19 | // 'a is used in two spots: 20 | // 21 | // | | 22 | // v v 23 | list: &'a Vec>, 24 | // 25 | // But since both of them are covariant (in immutable positions), 'a is 26 | // covariant as well. This means that the compiler can internally transform 27 | // &'a Vec> into the shorter &'a Vec>, and hold the 28 | // list for the shorter 'a duration. 29 | } 30 | // ANCHOR_END: SimpleMessageDisplayer 31 | 32 | // ANCHOR: MessageCollector 33 | struct MessageCollector<'a, 'msg> { 34 | // Two lifetime parameters, again: 35 | list: &'a mut Vec>, 36 | // Here, 'a is covariant, but 'msg is invariant since it is "inside" 37 | // a &mut reference. The compiler can vary the two independently, which 38 | // means that the list can be held onto for a shorter lifetime than 'msg. 39 | } 40 | // ANCHOR_END: MessageCollector 41 | 42 | // ANCHOR: SimpleMessageCollector 43 | struct SimpleMessageCollector<'a> { 44 | // 'a is used in two spots again: 45 | // 46 | // | | 47 | // v v 48 | list: &'a mut Vec>, 49 | // 50 | // The first 'a is covariant, but the second one is invariant since it is 51 | // "inside" a &mut reference! This means that 'a is invariant, and this 52 | // ends up causing the compiler to try and hold on to the list for longer 53 | // than with the standard MessageCollector. 54 | } 55 | // ANCHOR_END: SimpleMessageCollector 56 | -------------------------------------------------------------------------------- /code/ch01-04-variance-in-practice/simple-message-collector/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple-message-collector" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-04-variance-in-practice/simple-message-collector/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | use std::collections::HashSet; 5 | use std::fmt; 6 | 7 | struct Message<'msg> { 8 | message: &'msg str, 9 | } 10 | 11 | struct MessageCollector<'a, 'msg> { 12 | list: &'a mut Vec>, 13 | } 14 | 15 | impl<'a, 'msg> MessageCollector<'a, 'msg> { 16 | fn add_message(&mut self, message: Message<'msg>) { 17 | self.list.push(message); 18 | } 19 | } 20 | 21 | struct SimpleMessageDisplayer<'a> { 22 | list: &'a Vec>, 23 | } 24 | 25 | impl<'a> fmt::Display for SimpleMessageDisplayer<'a> { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | for message in self.list { 28 | write!(f, "{}\n", message.message)?; 29 | } 30 | Ok(()) 31 | } 32 | } 33 | 34 | // ANCHOR: SimpleMessageCollector 35 | struct SimpleMessageCollector<'a> { 36 | list: &'a mut Vec>, 37 | } 38 | 39 | impl<'a> SimpleMessageCollector<'a> { 40 | // This adds a message to the end of the list. 41 | fn add_message(&mut self, message: Message<'a>) { 42 | self.list.push(message); 43 | } 44 | } 45 | 46 | fn collect_and_display_3<'msg>(message_pool: &'msg HashSet) { 47 | // OK, one more time. 48 | let mut list = vec![]; 49 | 50 | // Collect some messages. 51 | let mut collector = SimpleMessageCollector { list: &mut list }; 52 | for message in message_pool { 53 | collector.add_message(Message { message }); 54 | } 55 | 56 | // Finally, display them. 57 | let displayer = SimpleMessageDisplayer { list: &list }; 58 | println!("{}", displayer); 59 | } 60 | // ANCHOR_END: SimpleMessageCollector -------------------------------------------------------------------------------- /code/ch01-04-variance-in-practice/simple-message-displayer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple-message-displayer" 3 | version = "0.1.0" 4 | authors = ["Rain "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /code/ch01-04-variance-in-practice/simple-message-displayer/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | fn main() {} 3 | 4 | use std::collections::HashSet; 5 | use std::fmt; 6 | 7 | struct Message<'msg> { 8 | message: &'msg str, 9 | } 10 | 11 | struct MessageCollector<'a, 'msg> { 12 | list: &'a mut Vec>, 13 | } 14 | 15 | impl<'a, 'msg> MessageCollector<'a, 'msg> { 16 | fn add_message(&mut self, message: Message<'msg>) { 17 | self.list.push(message); 18 | } 19 | } 20 | 21 | // ANCHOR: SimpleMessageDisplayer 22 | struct SimpleMessageDisplayer<'a> { 23 | list: &'a Vec>, 24 | } 25 | 26 | impl<'a> fmt::Display for SimpleMessageDisplayer<'a> { 27 | // This displays all the messages. 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | for message in self.list { 30 | write!(f, "{}\n", message.message)?; 31 | } 32 | Ok(()) 33 | } 34 | } 35 | 36 | fn collect_and_display_2<'msg>(message_pool: &'msg HashSet) { 37 | // OK, let's do the same thing as collect_and_display, except using the 38 | // simple displayer. 39 | let mut list = vec![]; 40 | 41 | // Collect some messages. 42 | let mut collector = MessageCollector { list: &mut list }; 43 | for message in message_pool { 44 | collector.add_message(Message { message }); 45 | } 46 | 47 | // Finally, display them. 48 | let displayer = SimpleMessageDisplayer { list: &list }; 49 | println!("{}", displayer); 50 | } 51 | // ANCHOR_END: SimpleMessageDisplayer -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Lifetime variance in Rust](../README.md) 4 | - [Building an intuition](./ch01-01-building-an-intuition.md) 5 | - [Formalizing variance](./ch01-02-formalizing-variance.md) 6 | - [Conflicts and type parameters](./ch01-03-conflicts-and-type-parameters.md) 7 | - [Variance in practice](./ch01-04-variance-in-practice.md) 8 | - [Epilogue](./ch01-05-epilogue.md) 9 | - [Acknowledgements](./ch02-00-acknowledgements.md) 10 | -------------------------------------------------------------------------------- /src/ch01-01-building-an-intuition.md: -------------------------------------------------------------------------------- 1 | # Building an intuition 2 | 3 | Consider this somewhat contrived function that takes a static string and makes 4 | its lifetime shorter: 5 | ```rust 6 | {{#rustdoc_include ../code/ch01-01-building-an-intuition/lifetime-shortener/src/main.rs:all}} 7 | ``` 8 | 9 | 10 | 11 | Intuitively, this feels like it should compile: if a string lasts for the whole 12 | process it should also last for any part of it. And it does! 13 | 14 | Now let's make it a bit more complicated. Consider a mutable reference to a `HashSet`. 15 | 16 | ```rust,does_not_compile 17 | {{#rustdoc_include ../code/ch01-01-building-an-intuition/cell-shortener/src/main.rs:all}} 18 | ``` 19 | 20 | `hash_set_shortener` doesn't compile! 21 | 22 | Can you tell why? Think about it for a minute, try using your intuition... 23 | 24 | ```rust,does_not_compile 25 | {{#rustdoc_include ../code/ch01-01-building-an-intuition/cell-example/src/main.rs:all}} 26 | ``` 27 | 28 | As a counterexample: 29 | 30 | ```rust,does_not_compile 31 | {{#rustdoc_include ../code/ch01-01-building-an-intuition/cell-counterexample/src/main.rs:all}} 32 | ``` 33 | 34 | It isn't just `&mut` which is problematic in this way. This also occurs with any sort of interior 35 | mutability, like `RefCell`, `OnceCell`, or `Mutex` -- anything inside some sort of mutable context 36 | has this issue. 37 | 38 | Now, what about a hypothetical "lengthener" function? 39 | ```rust,does_not_compile 40 | {{#rustdoc_include ../code/ch01-01-building-an-intuition/lifetime-lengthener/src/main.rs:all}} 41 | ``` 42 | 43 | This is clearly bogus, right? You can't just turn an arbitrary borrowed string 44 | and make it last the duration of the entire process. Similarly: 45 | ```rust,does_not_compile 46 | {{#rustdoc_include ../code/ch01-01-building-an-intuition/cell-lengthener/src/main.rs:all}} 47 | ``` 48 | 49 | But what about this? fn is a pointer to a function that takes an arbitrary 50 | borrowed string. 51 | ```rust 52 | {{#rustdoc_include ../code/ch01-01-building-an-intuition/fn-ptr-lengthener/src/main.rs:all}} 53 | ``` 54 | 55 | This feels like it should work. You can take a callback that takes an arbitrary borrowed string and 56 | turn it into one that takes in a static string, since you're weakening the guarantee. And it does. 57 | 58 | How can we handle these different cases in a principled way? That's where variance comes in. We're 59 | going to talk about this in the next chapter, *[Formalizing variance](ch01-02-formalizing-variance.md)*. 60 | -------------------------------------------------------------------------------- /src/ch01-02-formalizing-variance.md: -------------------------------------------------------------------------------- 1 | # Formalizing variance 2 | 3 | Some kinds of memory live longer than others. This is captured through the idea 4 | of the *outlives* relationship. If `'b` outlives `'a`, it is written as `'b: 'a`. 5 | For example, in the definition: 6 | ```rust,norun,noplayground 7 | {{#rustdoc_include ../code/ch01-02-formalizing-variance/outlives-example/src/main.rs:all}} 8 | ``` 9 | the borrowed string `b_str` lives at least as long as `a_str`, and possibly longer. 10 | 11 |   12 | 13 | The Rust compiler annotates every lifetime parameter with one of three settings. 14 | For a type `T<'a>`, `'a` may be: 15 | * **covariant**, which means that if `'b: 'a` then `T<'b>: T<'a>`. This is the 16 | default for immutable data. 17 | 18 | * **invariant**, which means that even if `'b: 'a`, nothing can be said about 19 | the relationship between `T<'b>` and `T<'a>`. This can happen for one of two reasons: 20 | * If the lifetime is present "inside" some sort of mutable context -- whether 21 | a `&mut` reference, or interior mutability like `RefCell`, `OnceCell`, or`Mutex`. 22 | 23 | * If the lifetime is used in multiple spots where the variances conflict. 24 | See [Conflicts and type parameters](./ch01-03-conflicts-and-type-parameters.md) for 25 | an example. 26 | 27 | * **contravariant**, which means that if `'b: 'a` then `T<'a>: T<'b>`. This is 28 | uncommon and only shows up in parameters to `fn` pointers. 29 | 30 | The variance of a parameter is determined entirely through the type definition. 31 | There's no marker trait for this. 32 | 33 | ## Quick exercise 34 | In the struct below, what are the variances of each lifetime parameter? 35 | ```rust,norun,noplayground 36 | {{#rustdoc_include ../code/ch01-02-formalizing-variance/quick-exercise/src/main.rs:Multi}} 37 | ``` 38 | 39 | ### The answers 40 | * `'a` is *covariant*, because it only shows up in an immutable context. 41 | This means that, similar to the shortener functions above, you can define a function like: 42 | ```rust 43 | {{#rustdoc_include ../code/ch01-02-formalizing-variance/quick-exercise/src/main.rs:a}} 44 | ``` 45 | 46 | * `'b` is *invariant*, because it is "inside" the mutable `Cell` context. 47 | > **Exercise**: try writing a function that fails to compile because `'b` is *invariant*. 48 | 49 | * `'c` is *contravariant*, because it shows up in the parameter to a callback. 50 | ```rust 51 | {{#rustdoc_include ../code/ch01-02-formalizing-variance/quick-exercise/src/main.rs:c}} 52 | ``` 53 | 54 | * `'d1` is *covariant*! Even though it is a mutable reference, it is not "inside" the `&mut` pointer. 55 | ```rust 56 | {{#rustdoc_include ../code/ch01-02-formalizing-variance/quick-exercise/src/main.rs:d1}} 57 | ``` 58 | 59 | * `'d2` is *invariant*, because it is "inside" a `&mut` reference. 60 | -------------------------------------------------------------------------------- /src/ch01-03-conflicts-and-type-parameters.md: -------------------------------------------------------------------------------- 1 | # Conflicts and type parameters 2 | 3 | What if a lifetime parameter is used in multiple spots with different variances? 4 | For example: 5 | ```rust,norun,noplayground 6 | {{#rustdoc_include ../code/ch01-03-conflicts-and-type-parameters/two-spots/src/main.rs:all}} 7 | ``` 8 | 9 | It's as you might expect: 10 | * If all the uses agree on a particular variance, the parameter has that variance. 11 | * Otherwise, the parameter defaults to *invariant*. 12 | 13 | And what about this sort of situation? 14 | ```rust,norun,noplayground 15 | {{#rustdoc_include ../code/ch01-03-conflicts-and-type-parameters/lifetime-check/src/main.rs:TypeParams}} 16 | ``` 17 | 18 | `T` and `U` are also annotated with a variance, which is used if they're 19 | substituted with a type containing a lifetime parameter. For example: 20 | ```rust,norun,noplayground 21 | {{#rustdoc_include ../code/ch01-03-conflicts-and-type-parameters/lifetime-check/src/main.rs:LifetimeParams}} 22 | ``` 23 | 24 | Here, `'a` is *covariant* and `'b` is *contravariant*. Let's test those together: 25 | ```rust 26 | {{#rustdoc_include ../code/ch01-03-conflicts-and-type-parameters/lifetime-check/src/main.rs:lifetime_check}} 27 | ``` -------------------------------------------------------------------------------- /src/ch01-04-variance-in-practice.md: -------------------------------------------------------------------------------- 1 | # Variance in practice 2 | 3 | So why should you, as a Rust developer, care? 4 | 5 | Many Rust developers start off by using reference counted smart pointers like 6 | `Rc` or `Arc` instead of borrowed data everywhere. If you're doing that, you're 7 | unlikely to run into lifetime issues. But you may eventually want to switch to 8 | borrowed data to get maximum performance -- if so, you'll probably have to introduce 9 | lifetime parameters into your code. That's when variance becomes important. Some 10 | of the thorniest issues getting rustc to accept code with pervasive use of borrowed 11 | data end up boiling down to variance in some fashion. 12 | 13 | For example, consider this situation, extracted from some real-world Rust code: 14 | ```rust 15 | {{#rustdoc_include ../code/ch01-04-variance-in-practice/message-displayer-and-collector/src/main.rs:all}} 16 | ``` 17 | This works, but can it be simplified? 18 | 19 | Let's try reducing the number of lifetime parameters, first for the displayer. 20 | ```rust 21 | {{#rustdoc_include ../code/ch01-04-variance-in-practice/simple-message-displayer/src/main.rs:SimpleMessageDisplayer}} 22 | ``` 23 | 24 | OK, that worked. Can we do the same for the collector? Let's try it out: 25 | ```rust,does_not_compile 26 | {{#rustdoc_include ../code/ch01-04-variance-in-practice/simple-message-collector/src/main.rs:SimpleMessageCollector}} 27 | ``` 28 | 29 | That doesn't work! rustc (as of 1.43.1) errors out with ``cannot borrow `list` as 30 | immutable because it is also borrowed as mutable``. 31 | 32 | Why did reducing the number of lifetime params work for `MessageDisplayer` but not 33 | `MessageCollector`? It's all because of variance. Let's have a look at the structs 34 | again, first the displayer: 35 | ```rust,norun,noplayground 36 | {{#rustdoc_include ../code/ch01-04-variance-in-practice/overview/src/main.rs:MessageDisplayer}} 37 | ``` 38 | 39 | The simple version: 40 | ```rust,norun,noplayground 41 | {{#rustdoc_include ../code/ch01-04-variance-in-practice/overview/src/main.rs:SimpleMessageDisplayer}} 42 | ``` 43 | 44 | Now the collector: 45 | ```rust,norun,noplayground 46 | {{#rustdoc_include ../code/ch01-04-variance-in-practice/overview/src/main.rs:MessageCollector}} 47 | ``` 48 | 49 | Finally, the problematic simple version: 50 | ```rust,norun,noplayground 51 | {{#rustdoc_include ../code/ch01-04-variance-in-practice/overview/src/main.rs:SimpleMessageCollector}} 52 | ``` 53 | 54 | ### A final note if you're writing a Rust library 55 | Changing the variance of a parameter (lifetime or type) from *covariant* to 56 | anything else, or from *contravariant* to anything else, is a BREAKING CHANGE. 57 | If you're following semver, it can only be done with a new major version. 58 | 59 | Changing a parameter from *invariant* to *co-* or *contravariant* is not a breaking change. 60 | -------------------------------------------------------------------------------- /src/ch01-05-epilogue.md: -------------------------------------------------------------------------------- 1 | # Epilogue 2 | 3 | Hope this made you feel more confident using lifetimes in your Rust code! 4 | They're a very powerful way to write safe, blazing fast code. But variance can 5 | often cause obscure issues in practice -- knowledge of how it works is key to 6 | using lifetimes effectively. 7 | -------------------------------------------------------------------------------- /src/ch02-00-acknowledgements.md: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | Thanks to the following people for their feedback: 3 | * Nikolai Vazquez ([Twitter](https://twitter.com/NikolaiVazquez), [GitHub](https://github.com/nvzqz)) 4 | * Inanna Malick ([Twitter](https://twitter.com/inanna_malick), [GitHub](https://github.com/inanna-malick)) 5 | 6 | And thanks to Berkay Dinç ([GitHub](https://github.com/brkydnc)) for converting the tutorial over to mdbook. 7 | -------------------------------------------------------------------------------- /src/img/does_not_compile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /theme/head.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------