CRDoc::Cheats
Built-in cheat sheets, baked in from src/crdoc/cheats/*.md.
Each file starts with YAML frontmatter (name, aliases, title,
see_also) between --- lines, followed by a Markdown body.
Constants
FILES = ["blocks", "c", "case", "collections", "concurrency", "enums", "exceptions", "generics", "json", "macros", "nil", "strings", "structs", "syntax"] of ::String
Topic file basenames. A spec checks this against the directory.
FRONTMATTER = /\A---\n(.*?)\n---\n(.*)\z/m
SOURCES = {"blocks" => "---\nname: blocks\naliases: [procs, yield, block, closures]\ntitle: Blocks and procs\nsee_also: [\"Proc\", \"Proc#call\", \"Object#try\"]\n---\n\nA method that uses `yield` inlines its block, so no closure is allocated and\n`break` and `return` inside the block act on the caller. A method that names\nthe block (`&block`) captures it as a `Proc`; the block then needs a full type,\nand exits with `next value` instead of `return`.\n\n```crystal\n# Yields an Int32 and expects an Int32 back.\ndef twice(& : Int32 -> Int32) : Array(Int32)\n [yield(1), yield(2)]\nend\n\n# Captures the block. `String ->` means it returns nothing.\ndef on_event(&block : String ->) : Proc(String, Nil)\n block\nend\n\np twice { |number| number * 10 } # => [10, 20]\nhandler = on_event { |event| puts \"got \#{event}\" }\nhandler.call(\"click\")\n\nsquare = ->(number : Int32) { number * number } # Proc(Int32, Int32)\np square.call(4) # => 16\np [1, 2, 3].map(&square) # a proc as the block\np [1, 2, 3].map(&.to_s) # same as { |number| number.to_s }\n\ndef shout(text : String) : String\n text.upcase\nend\n\nloud = ->shout(String) # a proc from a method\np loud.call(\"hi\") # => \"HI\"\n```\n", "c" => "---\nname: c\naliases: [lib, ffi, bindings]\ntitle: C bindings\nsee_also: [\"Link\", \"Pointer\", \"Slice\", \"pointerof\", \"sizeof\"]\n---\n\nA `lib` declares C functions (`fun`), structs and types. `@[Link(\"name\")]`\nlinks `-lname`. An argument whose type does not match the `fun` signature is\nconverted by calling its `to_unsafe`; `String` returns a `UInt8*` that way.\n`out` passes a pointer to a fresh variable.\n\n```crystal\n@[Link(\"m\")]\nlib LibDemo\n struct LLDivResult # matches C's lldiv_t\n quot : LibC::LongLong\n rem : LibC::LongLong\n end\n\n fun lldiv(numerator : LibC::LongLong, denominator : LibC::LongLong) : LLDivResult\n fun strlen(text : LibC::Char*) : LibC::SizeT\n fun frexp(value : LibC::Double, exponent : LibC::Int*) : LibC::Double\nend\n\nclass Name\n def initialize(@text : String)\n end\n\n def to_unsafe : UInt8*\n @text.to_unsafe\n end\nend\n\nresult = LibDemo.lldiv(17, 5)\np result.quot, result.rem # => 3, 2\np LibDemo.strlen(\"hello\") # => 5, via String#to_unsafe\np LibDemo.strlen(Name.new(\"crdoc\")) # => 5, via Name#to_unsafe\np LibDemo.frexp(8.0, out exponent), exponent # => 0.5, 4\n\nvalue = 42\npointer = pointerof(value) # Int32*\npointer.value += 1\np value # => 43\nbytes = Slice.new(Pointer(UInt8).malloc(4), 4) # a bounded view of raw memory\np bytes.size # => 4\n```\n", "case" => "---\nname: case\naliases: [in]\ntitle: case ... in and case ... when\nsee_also: [\"Enum\", \"Object#===\", \"Object#is_a?\"]\n---\n\n`case ... in` is exhaustive. The compiler rejects it unless every type in a\nunion, every enum member, or both `true` and `false` are covered, and it takes\nno `else`. `case ... when` matches with `===` and needs `else` for leftovers.\n\n```crystal\nenum Color\n Red\n Green\n Blue\nend\n\ndef describe(value : Int32 | String | Nil) : String\n case value\n in Int32 then \"int \#{value + 1}\" # value narrowed to Int32\n in String then \"string of \#{value.size}\"\n in Nil then \"nothing\"\n end\nend\n\ndef warm?(color : Color) : Bool\n case color\n in .red? then true # enum predicate methods\n in .green?, .blue? then false\n end\nend\n\ndef size_name(size : Int32) : String\n case size\n when 0 then \"empty\"\n when 1..9 then \"small\" # Range#===\n when .negative? then \"invalid\"\n else \"large\"\n end\nend\n\nputs describe(\"hi\"), warm?(:red), size_name(5)\n```\n", "collections" => "---\nname: collections\naliases: [containers]\ntitle: Arrays, hashes, sets and tuples\nsee_also: [\"Array\", \"Hash\", \"Set\", \"Tuple\", \"NamedTuple\", \"Enumerable\", \"Indexable\"]\n---\n\nEmpty literals need a type (`[] of Int32`). `[]` raises on a missing index or\nkey and `[]?` returns `nil`. Methods ending in `!` change the receiver.\nTuples are fixed-size, immutable, and typed per position.\n\n```crystal\nnumbers = [3, 1, 2] # Array(Int32)\nempty = [] of String\nages = {\"ada\" => 36, \"alan\" => 41} # Hash(String, Int32)\ncounts = Hash(String, Int32).new(0) # 0 for missing keys\nunique = Set{1, 2, 2} # Set(Int32) with 2 items\npoint = {1, \"a\"} # Tuple(Int32, String)\noptions = {verbose: true, level: 2} # NamedTuple\n\nnumbers << 4\nempty.push(\"x\")\np numbers[10]?, ages[\"bob\"]? # => nil, nil\np numbers.sort, numbers.sum # => [1, 2, 3, 4], 10\np numbers.map { |number| number * 2 }.select(&.> 4) # => [6, 8]\np numbers.each_slice(2).to_a # => [[3, 1], [2, 4]]\np numbers.sort_by!(&.-) # => [4, 3, 2, 1], in place\np %w(a b a).tally # => {\"a\" => 2, \"b\" => 1}\np numbers.group_by(&.even?) # => {true => [4, 2], false => [3, 1]}\n\nages.each { |name, age| counts[name] += age }\np ages.fetch(\"bob\", 0), ages.keys # => 0, [\"ada\", \"alan\"]\np ages.transform_values(&.succ) # => {\"ada\" => 37, \"alan\" => 42}\np unique.includes?(2), point[1], options[:level] # => true, \"a\", 2\n```\n", "concurrency" => "---\nname: concurrency\naliases: [threads, parallel, fibers]\ntitle: Concurrency and execution contexts\nsee_also:\n - \"Fiber::ExecutionContext\"\n - \"Fiber::ExecutionContext::Parallel\"\n - \"Fiber::ExecutionContext::Concurrent\"\n - \"Fiber::ExecutionContext::Isolated\"\n - \"Fiber::ExecutionContext.default_workers_count\"\n - \"Channel\"\n - \"WaitGroup\"\n - \"Atomic\"\n - \"Mutex\"\n - \"spawn\"\n---\n\nA fiber belongs to an execution context, not a thread. `spawn` starts a fiber\nin the current context; `context.spawn` targets another. No compile flag is\nneeded. The deprecated `-Dpreview_mt` removes these types entirely.\n\n* The default context is `Parallel` with parallelism 1. Grow it with\n `Fiber::ExecutionContext.default.resize(count)`; `default_workers_count`\n returns `CRYSTAL_WORKERS` or the CPU count.\n* `Parallel.new(name, maximum)` runs CPU-bound work on up to *maximum* threads.\n* `Concurrent.new(name)` runs many fibers, one at a time.\n* `Isolated.new(name) { }` gives one fiber its own thread, for blocking or\n thread-affine calls. `#wait` joins it and re-raises its exception.\n\n```crystal\nrequire \"wait_group\"\n\ndef fib(number : Int32) : Int64\n number < 2 ? number.to_i64 : fib(number - 1) + fib(number - 2)\nend\n\nworkers = Fiber::ExecutionContext::Parallel.new(\"workers\", 4)\nresults = Channel({Int32, Int64}).new\ninputs = [30, 31, 32, 33]\ninputs.each do |number|\n workers.spawn { results.send({number, fib(number)}) }\nend\ninputs.size.times do\n number, value = results.receive\n puts \"fib(\#{number}) = \#{value}\" # arrival order varies\nend\n\nblocker = Fiber::ExecutionContext::Isolated.new(\"blocker\") do\n sleep 100.milliseconds # stands in for a blocking C call\nend\nblocker.wait\n\nWaitGroup.wait do |group| # waits for fibers spawned in this context\n 3.times { |index| group.spawn { puts \"task \#{index}\" } }\nend\n```\n", "enums" => "---\nname: enums\naliases: [enum]\ntitle: Enums and flags\nsee_also: [\"Enum\", \"Enum.parse?\", \"Enum.from_value?\", \"Enum.each\", \"Flags\"]\n---\n\nEach member gets a predicate method (`Color::Red.red?`). A symbol autocasts to\nan enum member where a method argument is restricted to a single enum type.\n`@[Flags]` makes values powers of two and adds `None` and `All`.\n\n```crystal\nenum Color\n Red\n Green\n Blue = 10 # explicit value\n\n def warm? : Bool\n red?\n end\nend\n\n@[Flags]\nenum Access\n Read\n Write\n Exec\nend\n\ndef paint(color : Color) : String\n \"painting \#{color.to_s.downcase}\"\nend\n\nputs paint(:green) # symbol autocast\np Color::Red.value # => 0\np Color.parse(\"blue\") # => Color::Blue, case-insensitive\np Color.parse?(\"pink\") # => nil\np Color.from_value?(10) # => Color::Blue\np Color.values.map(&.warm?) # => [true, false, false]\n\naccess = Access::Read | Access::Write\np access.write?, access.exec? # => true, false\np access # => Access[Read, Write]\np Access::All.includes?(Access::Exec) # => true\n```\n", "exceptions" => "---\nname: exceptions\naliases: [rescue, ensure, errors]\ntitle: Exceptions\nsee_also: [\"Exception\", \"raise\", \"Exception#message\", \"Exception#inspect_with_backtrace\"]\n---\n\nSubclass `Exception` for your own errors. A `def` body can take `rescue`,\n`else` and `ensure` directly, without `begin`. Crystal has no `retry`. Many\nstdlib methods have a `?` variant that returns `nil` instead of raising\n(`to_i?`, `[]?`, `parse?`).\n\n```crystal\nclass ConfigError < Exception\n getter path : String\n\n def initialize(@path : String, message : String? = nil)\n super(message || \"bad config: \#{path}\")\n end\nend\n\ndef load(path : String) : String\n raise ConfigError.new(path) if path.empty?\n File.read(path)\nrescue error : File::NotFoundError # method-level rescue\n raise ConfigError.new(path, \"missing: \#{path}\")\nend\n\nbegin\n load(\"/no/such/file\")\nrescue error : ConfigError\n puts \"\#{error.class}: \#{error.message}\"\nrescue error : IO::Error | ArgumentError # a union of types\n puts \"I/O problem: \#{error.message}\"\nrescue error # any Exception\n raise error # re-raise\nelse\n puts \"no exception\"\nensure\n puts \"always runs\"\nend\n\nraise \"plain message\" if ARGV.includes?(\"--fail\") # raises Exception\n```\n", "generics" => "---\nname: generics\naliases: [generic, forall]\ntitle: Generics\nsee_also: [\"Array\", \"Hash\", \"Tuple\", \"typeof\"]\n---\n\nType parameters go in parentheses and are usually inferred from constructor\narguments. A method introduces its own type variables with `forall`.\n\n```crystal\nclass Holder(T)\n getter value : T\n\n def initialize(@value : T)\n end\n\n # U is bound from the block's return type.\n def map(& : T -> U) : Holder(U) forall U\n Holder.new(yield value)\n end\nend\n\ndef first_or(items : Array(T), fallback : T) : T forall T\n items.empty? ? fallback : items.first\nend\n\ndef pair(left : K, right : V) : Hash(K, V) forall K, V\n {left => right}\nend\n\nholder = Holder.new(42) # Holder(Int32), T inferred\ntext = holder.map(&.to_s) # Holder(String)\ntyped = Holder(Int32 | String).new(\"hi\")\ntyped = Holder(Int32 | String).new(3) # same type, so it can be reassigned\n\np text.value, typed.value # => \"42\", 3\np first_or([] of Int32, 0) # => 0\np pair(:port, 8080) # => {:port => 8080}\np typeof(holder), typeof(pair(\"a\", 1.5)) # => Holder(Int32), Hash(String, Float64)\n\nclass IntHolder < Holder(Int32) # inherit from an instantiation\nend\n```\n", "json" => "---\nname: json\naliases: [yaml, serializable, serialization]\ntitle: JSON and YAML serialization\nsee_also: [\"JSON::Serializable\", \"JSON::Field\", \"JSON.parse\", \"JSON::Any\", \"YAML::Serializable\", \"YAML::Field\"]\n---\n\nInclude `JSON::Serializable` to get `from_json` and `to_json` built from the\ninstance variables. Nilable fields may be absent, fields with defaults are\noptional, and `nil` values are left out of the output. `YAML::Serializable`\nand `@[YAML::Field]` work the same way.\n\n```crystal\nrequire \"json\"\nrequire \"yaml\"\n\nstruct Server\n include JSON::Serializable\n include YAML::Serializable\n\n getter host : String\n\n @[JSON::Field(key: \"port_number\")]\n getter port : Int32 = 80\n\n getter tags : Array(String)? # may be missing or null\n\n @[JSON::Field(ignore: true)]\n @[YAML::Field(ignore: true)]\n getter connections = 0\nend\n\nserver = Server.from_json(%({\"host\": \"example.com\", \"port_number\": 8080}))\np server.port # => 8080\nputs server.to_json # => {\"host\":\"example.com\",\"port_number\":8080}\n\nservers = Array(Server).from_yaml(\"- host: a\\n- host: b\\n tags: [x]\")\np servers.map(&.port) # => [80, 80]\n\nany = JSON.parse(%({\"list\": [1, 2]})) # untyped: JSON::Any\np any[\"list\"][1].as_i # => 2\np Hash(String, Int32).from_json(%({\"a\": 1}))\n```\n", "macros" => "---\nname: macros\naliases: [macro, annotations, metaprogramming]\ntitle: Macros and annotations\nsee_also: [\"Crystal::Macros\", \"Crystal::Macros::TypeNode\", \"Crystal::Macros::ASTNode#id\", \"Crystal::Macros::Annotation\"]\n---\n\nMacros run at compile time and paste code. `{{ expr }}` inserts a value and\n`{% stmt %}` runs logic. `.id` pastes an argument as an identifier and\n`.stringify` as a string. In a method, `@type` is the current type.\n\n```crystal\nmacro define_flags(*names)\n {% for name in names %}\n def {{ name.id }}? : Bool\n @flags.includes?({{ name.stringify }})\n end\n {% end %}\nend\n\nannotation Column\nend\n\nclass User\n getter flags = [\"admin\"]\n @[Column(name: \"user_name\")]\n getter name : String = \"ada\"\n\n define_flags admin, banned\n\n def columns : Array(String)\n {% begin %}\n [\n {% for ivar in @type.instance_vars %}\n {% if column = ivar.annotation(Column) %}\n {{ column[:name] }},\n {% else %}\n {{ ivar.name.stringify }},\n {% end %}\n {% end %}\n ]\n {% end %}\n end\nend\n\np User.new.admin? # => true\np User.new.columns # => [\"user_name\", \"flags\"]\n```\n", "nil" => "---\nname: nil\naliases: [nilable, nillable]\ntitle: Nil and nilable types\nsee_also: [\"Nil\", \"Object#try\", \"Object#nil?\", \"Object#not_nil!\"]\n---\n\n`T?` is shorthand for `T | Nil`. The compiler narrows a local variable after\na truthiness or `.nil?` check. It never narrows a method call, so copy the\nresult into a local first. Avoid `not_nil!`: it turns a compile-time type\nerror into a runtime `NilAssertionError`.\n\n```crystal\ndef find_user(id : Int32) : String?\n id == 1 ? \"ada\" : nil\nend\n\nclass Account\n getter owner : String? = nil\n\n def label : String\n if owner = self.owner # assign and test; owner is String inside\n \"owned by \#{owner}\"\n else\n \"unowned\"\n end\n end\nend\n\ndef initial(id : Int32) : Char\n user = find_user(id)\n return '?' if user.nil? # user is String below\n user[0]\nend\n\nputs find_user(2) || \"anonymous\" # fallback value\np find_user(1).try(&.size) # => 3, or nil for a nil receiver\np initial(1), Account.new.label\n```\n", "strings" => "---\nname: strings\naliases: [heredoc, heredocs]\ntitle: Strings\nsee_also: [\"String\", \"String.build\", \"Char\", \"Regex\", \"String::Builder\"]\n---\n\nStrings are immutable UTF-8 and use double quotes; `'a'` is a `Char`.\n`size` counts characters and `bytesize` counts bytes. Build large strings with\n`String.build` instead of repeated `+`.\n\n```crystal\nname = \"Ada\"\ngreeting = \"Hello, \#{name}!\\n\" # interpolation and escapes\nquoted = %(She said \"hi\" to \#{name}) # %() allows unescaped quotes\nraw = %q(no \#{interpolation} here)\nwords = %w(alpha beta gamma) # Array(String)\n\nreport = <<-TEXT\n Name: \#{name}\n indented line\n TEXT\n# The closing TEXT's indentation is stripped from every line.\n\nbuilt = String.build do |io|\n words.each_with_index { |word, index| io << index << '=' << word << ' ' }\nend\n\nprint greeting\nputs quoted, raw, report, built.rstrip\np \"a,b,,c\".split(','), \" pad \".strip # => [\"a\", \"b\", \"\", \"c\"], \"pad\"\np \"héllo\".size, \"héllo\".bytesize # => 5, 6\np \"hello\".sub('l', 'L'), \"hello\".gsub(/l+/, \"_\") # => \"heLlo\", \"he_o\"\np \"42\".to_i, \"4x\".to_i?, \"%.2f\" % 3.14159 # => 42, nil, \"3.14\"\np name.starts_with?(\"A\"), name.includes?(\"d\"), name * 2\nif match = \"v1.21.1\".match(/(\\d+)\\.(\\d+)/)\n p match[1], match[2] # => \"1\", \"21\"\nend\n```\n", "structs" => "---\nname: structs\naliases: [struct]\ntitle: Structs and records\nsee_also: [\"Struct\", \"Value\", \"Reference\", \"record\"]\n---\n\nA `class` is a heap-allocated reference with identity. A `struct` is a value:\nit is copied on assignment and when passed or returned, and it can inherit only\nfrom an abstract struct. Use structs for small, immutable data.\n\n```crystal\nstruct Point\n getter x : Int32\n getter y : Int32\n\n def initialize(@x : Int32, @y : Int32)\n end\n\n def +(other : Point) : Point\n Point.new(x + other.x, y + other.y)\n end\nend\n\nstruct Counter\n property count = 0\nend\n\ncounters = [Counter.new]\ncounters[0].count += 1 # updates a copy returned by Array#[]\np counters[0].count # => 0\n\n# record defines a struct with getters, initialize, ==, hash and copy_with.\nrecord Money, cents : Int64, currency : String = \"USD\" do\n def to_s(io : IO) : Nil\n io << currency << ' ' << cents // 100 << '.' << (cents % 100).to_s.rjust(2, '0')\n end\nend\n\nprice = Money.new(1999)\nputs price, price.copy_with(currency: \"EUR\") # => USD 19.99, EUR 19.99\np Point.new(1, 2) + Point.new(3, 4) # => Point(@x=4, @y=6)\n```\n", "syntax" => "---\nname: syntax\naliases: [basics]\ntitle: Basic syntax\nsee_also: [\"Object.getter\", \"Object.property\", \"Range\", \"String\", \"Int32\"]\n---\n\nTypes are inferred; restrictions on arguments and return types are optional\nbut checked. Instance variables are declared through `getter` or `property`.\n\n```crystal\nname = \"Crystal\" # String, inferred\ncount : Int32 = 3 # explicit type\nbig = 10_i64 # literal suffix picks the type\nLIMIT = 5 # constant\n\ndef greet(who : String, times : Int32 = 1) : String\n \"Hello, \#{who}! \" * times\nend\n\nclass Greeter\n getter name : String\n property visits = 0\n\n def initialize(@name : String) # @name assigned from the argument\n end\n\n def greet : String\n @visits += 1\n \"Hi, \#{name} (\#{visits})\"\n end\nend\n\nputs greet(name, times: 2)\nputs Greeter.new(\"Ada\").greet\n(1..3).each { |number| puts number } # 1..3 inclusive, 1...3 exclusive\nputs \"many\" if count > 2 # suffix if/unless work\nputs \"small\" unless big > 100\nwhile count < LIMIT # no suffix while/until\n count += 1\nend\n```\n"}
Raw file contents by basename, read at compile time.