diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e989ed3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,63 @@ +## Overview + +This repo contains code and data required to compile the **Quarto-based +documentation website** for the [extendr](https://github.com/extendr/extendr) +project — a framework for writing R packages using Rust. Published at +https://extendr.github.io. + +## Codebase + +**extendr** +repo: https://github.com/extendr/extendr +description: The core Rust library. Read source files there directly when you +need to understand extendr's API, types, macros, or implementation details. + +**rextendr** +repo: https://github.com/extendr/rextendr +description: The R package that scaffolds extendr projects and generates R +wrappers. Read source files there directly when you need to understand +`rextendr::use_extendr()`, template generation, or the wrapper generation +architecture. + +## Repository Structure + +``` +. +├── _quarto.yml # Site config: navbar, sidebars, theme +├── index.qmd # Landing page (3 cards: Get Started, Examples, Community) +├── get-started.qmd # 4-step install guide (Rust, R ≥ 4.2, rextendr, rust-analyzer) +├── changelog.qmd # Dynamically fetches extendr CHANGELOG.md from GitHub +├── user-guide/ # User guide +├── intro-rust/ # Intro to Rust for R developers +├── contributing/ # Contributor guide (style, colors, scaffolding) +├── blog/ # Blog posts +├── css/ # Custom SCSS +├── images/ # Image files +└── _extensions/ # Quarto extensions +``` + +## Quarto (`_quarto.yml`) + +This is a Quarto website. The main site structure and metadata are defined in +`_quarto.yml`, including the navbar, sidebars, theme, and page execution +settings. When adding or reorganizing pages, `_quarto.yml` is the first place to +look. + +## Content Sections + +- Get Started (`get-started.qmd`): An installation guide for setting up extendr + and rextendr. +- User Guide (`user-guide/`): The main guide for R package development with + extendr. +- Rust Basics (`intro-rust/`): An opinionated introduction to Rust for R + developers, covering the concepts needed to use extendr effectively. +- Contributing (`contributing/`): Guidelines for contributing to the extendr + documentation, including writing style, branding, and scaffolding details. + + +## Claude Behavior + +- DO NOT BE OBSEQUIOUS WITH PRAISE. +- BE CONCISE. Only elaborate when that is requested. +- When editing markdown files, please keep line length to 80 characters. +- DO NOT EDIT CLAUDE.md WITHOUT ASKING PERMISSION. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1cac98..047df3d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,139 +1,97 @@ # Contributing -We welcome contributions to the extendr project. Contributions come in many forms. Please carefully read and follow these guidelines. This will help us make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing this project. +Contributions to the extendr project can come in many forms, and we welcome all +of them. Please carefully read and follow the guidelines in our +[Contributing Guide](https://extendr.github.io/contributing/index.html). This +will help us make the contribution process easy and effective for everyone +involved. It also communicates that you agree to respect the time of the +developers managing and developing this project. +## Code of Conduct {#code-of-conduct} -## Quicklinks +We take our open source community seriously and hold ourselves and other +contributors to high standards of communication. By participating and +contributing to this project, you agree to uphold our [Code of +Conduct.](https://github.com/extendr/extendr/blob/master/CODE-OF-CONDUCT.md) -* [Code of Conduct](#code-of-conduct) -* [Getting Started](#getting-started) - * [Issues](#issues) - * [Pull Requests](#pull-requests) -* [Getting Help](#getting-help) -* [Authorship](#authorship) -* [Attribution](#attribution) +## Getting Started {#getting-started} -## Repo structure - -This is the source of the extendr project's website. - -- `user-guide` Is the user guide for using `extendr-api` -- `blog` is the source code for blogs for the extendr-project -- `intro-rust` contains souce code for an Introduction to Rust for R developers - -## Using extendr code chunks - -Whenever using extendr code, use the `extendrsrc` and `extendr` knitr engines. -The user-guide/serde-integration.qmd is the best example. Try to emulate this style. - -## User guide - -The user guide is a friendly introduction to the `extendr-api` crate. Any entries into the user guide should be **concise** and use **simple language**. Always use code chunks whenever using extendr code. - -The structure is roughly as follows: - -```` -``` ---- -title: "An informative title" ---- - -- Motivate the functionality that the entry will cover -- No more than 4 sentences - -## The feature - -- Describe the functionality that will be covered -- Discuss pre-reqs if any - -## Basic usage - -- Provide a basic example -- Discuss your code prior to showing the code - -```{extendrsrc} -// your rust code -``` - -- Summarise and explicate - -## Advanced usage - -- Motivate advanced usage -- Provide an example - -```{extendrsrc} -// your rust code -``` - -- Summarize and explicate - -## See also - -- Provide useful links to other docs on the site or externally -``` -```` - -### Writing style - -- use of second person is fine, but you cannot write about one's intention in the second person. -- Banned phrases ❌: - - "you want to" - - "you need to" -- never use `print()` to print an R object - - -## Code of Conduct - -We take our open source community seriously and hold ourselves and other contributors to high standards of communication. By participating and contributing to this project, you agree to uphold our [Code of Conduct.](https://github.com/extendr/extendr/blob/master/CODE-OF-CONDUCT.md) - -## Getting Started - -Contributions can be made via Issues and Pull Requests (PRs). A few general guidelines cover both: +Contributions can be made via Issues and Pull Requests (PRs). A few general +guidelines cover both: - Please search for existing Issues and PRs before creating your own. -- We work hard to makes sure issues are handled in a timely manner but, depending on the problem and maintainer availability, it could take a while to investigate the problem. A friendly ping in the comment thread can help draw attention if an issue has not received any attention for a while. Please keep in mind that all contributors to this project are volunteers and may have other commitments they need to attend to. +- We work hard to makes sure issues are handled in a timely manner but, + depending on the problem and maintainer availability, it could take a while + to investigate the problem. A friendly ping in the comment thread can help + draw attention if an issue has not received any attention for a while. + Please keep in mind that all contributors to this project are volunteers and + may have other commitments they need to attend to. -### Issues +### Issues {#issues} -Issues should be used to report problems with the library, request a new feature, or to discuss potential changes before a PR is created. Please **do not** use Issues to request user support. +Issues should be used to report problems with the library, request a new +feature, or to discuss potential changes before a PR is created. Please **do +not** use Issues to request user support. -Whenever possible, please provide a minimal reproducible example (reprex) to any bug report that you are filing. The more minimal your example, the more likely that somebody else can figure out what the problem is, so please remove any code that isn't relevant to the problem you are reporting. +Whenever possible, please provide a minimal reproducible example (reprex) to any +bug report that you are filing. The more minimal your example, the more likely +that somebody else can figure out what the problem is, so please remove any code +that isn't relevant to the problem you are reporting. -Please keep issues focused on one particular problem. Don't feel shy about opening multiple issues if you're encountering more than one problem. +Please keep issues focused on one particular problem. Don't feel shy about +opening multiple issues if you're encountering more than one problem. -If you find an Issue that addresses the problem you're having, please add your own reproduction information to the existing issue rather than creating a new one. Adding a [reaction](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) can also help be indicating to our maintainers that a particular problem is affecting more than just the reporter. +If you find an Issue that addresses the problem you're having, please add your +own reproduction information to the existing issue rather than creating a new +one. Adding a +[reaction](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) +can also help indicate to our maintainers that a particular problem is affecting +more than just the reporter. -### Pull Requests +### Pull Requests {#pull-requests} -PRs are always welcome and can be a quick way to get your fix or improvement slated for the next release. However, please always open an Issue before submitting a PR. +PRs are always welcome and can be a quick way to get your fix or improvement +slated for the next release. However, please always open an Issue before +submitting a PR. In general, PRs should: - Address a single concern in the least number of changed lines as possible. -- Only fix/add the functionality in question **OR** address wide-spread whitespace/style issues, not both. +- Only fix/add the functionality in question **OR** address wide-spread + whitespace/style issues, not both. - Add unit or integration tests for fixed or changed functionality. - Include documentation. -- Indicate which Issue they address by using the words `Closes #` or `Fixes #` in the body of the PR and/or the git commit message. (See the [GitHub Documentation](https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for details about linking PRs to Issues and automatically closing Issues when merging PRs.) - +- Indicate which Issue they address by using the words `Closes #` + or `Fixes #` in the body of the PR and/or the git commit + message. (See the [GitHub Documentation](https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) + for details about linking PRs to Issues and automatically closing Issues when + merging PRs.) -In general, we follow the [GitHub flow](https://guides.github.com/introduction/flow/index.html) development model: +In general, we follow the [GitHub flow](https://guides.github.com/introduction/flow/index.html) +development model: 1. Fork the repository to your own Github account 2. Clone the project to your machine 3. Create a branch locally with a succinct but descriptive name 4. Commit changes to the branch 5. Push changes to your fork -6. Open a PR in our repository and follow the PR template so that we can efficiently review the changes. +6. Open a PR in our repository and follow the PR template so that we can + efficiently review the changes. -## Getting Help +## Getting Help {#getting-help} -Please join us on our [Discord server](https://discord.gg/7hmApuc) for general conversations and questions that don't belong into a GitHub issue. +Please join us on our [Discord server](https://discord.gg/7hmApuc) for general +conversations and questions that don't belong in a GitHub issue. -## Authorship +## Authorship {#authorship} -Contributors who have made multiple, sustained, and/or non-trivial contributions to the project may be added to the author list. New author names will always be added at the end of the list, so that author order reflects chronological order of joining the project. All authorship decisions are at the discretion of the current maintainers of the project. +Contributors who have made multiple, sustained, and/or non-trivial contributions +to the project may be added to the author list. New author names will always be +added at the end of the list, so that author order reflects chronological order +of joining the project. All authorship decisions are at the discretion of the +current maintainers of the project. -## Attribution +## Attribution {#attribution} -This document was adapted from the [General Contributing Guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) of the auth0 project. +This document was adapted from the [General Contributing Guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) +of the auth0 project. \ No newline at end of file diff --git a/_extensions/mcanouil/iconify/LICENSE b/_extensions/mcanouil/iconify/LICENSE new file mode 100644 index 0000000..4b53bb8 --- /dev/null +++ b/_extensions/mcanouil/iconify/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mickaël Canouil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/_extensions/mcanouil/iconify/_extension.yml b/_extensions/mcanouil/iconify/_extension.yml new file mode 100644 index 0000000..72eca88 --- /dev/null +++ b/_extensions/mcanouil/iconify/_extension.yml @@ -0,0 +1,7 @@ +title: Iconify +author: Mickaël Canouil +version: 3.1.0 +quarto-required: ">=1.5.57" +contributes: + shortcodes: + - iconify.lua diff --git a/_extensions/mcanouil/iconify/_modules/utils.lua b/_extensions/mcanouil/iconify/_modules/utils.lua new file mode 100644 index 0000000..4ec88f1 --- /dev/null +++ b/_extensions/mcanouil/iconify/_modules/utils.lua @@ -0,0 +1,655 @@ +--- MC Utils - Common utility functions for Quarto Lua filters and shortcodes +--- @module utils +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil +--- @version 1.0.0 + +local M = {} + +-- ============================================================================ +-- STRING UTILITIES +-- ============================================================================ + +--- Pandoc utility function for converting values to strings +--- @type function +M.stringify = pandoc.utils.stringify + +--- Check if a string is empty or nil. +--- Utility function to determine if a value is empty or nil, +--- which is useful for parameter validation throughout the module. +--- @param s string|nil|table The value to check for emptiness +--- @return boolean True if the value is nil or empty, false otherwise +--- @usage local result = M.is_empty("") -- returns true +--- @usage local result = M.is_empty(nil) -- returns true +--- @usage local result = M.is_empty("hello") -- returns false +function M.is_empty(s) + return s == nil or s == '' +end + +--- Escape special pattern characters in a string for Lua pattern matching +--- @param s string The string to escape +--- @return string The escaped string +--- @usage local escaped = M.escape_pattern("user/repo#123") +function M.escape_pattern(s) + local escaped = s:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") + return escaped +end + +--- Split a string by a separator +--- @param str string The string to split +--- @param sep string The separator pattern +--- @return table Array of string fields +--- @usage local parts = M.split("a.b.c", ".") +function M.split(str, sep) + local fields = {} + local pattern = string.format("([^%s]+)", sep) + str:gsub(pattern, function(c) fields[#fields + 1] = c end) + return fields +end + +--- Trim leading and trailing whitespace from a string +--- @param str string The string to trim +--- @return string The trimmed string +--- @usage local trimmed = M.trim(" hello world ") -- returns "hello world" +function M.trim(str) + if str == nil then return '' end + return str:match('^%s*(.-)%s*$') +end + +--- Convert any value to a string, handling Pandoc objects and empty values. +--- Returns nil for empty or nil values, otherwise returns a string representation. +--- @param val any The value to convert +--- @return string|nil The string value or nil if empty +--- @usage local str = M.to_string(kwargs.value) +function M.to_string(val) + if not val then return nil end + if type(val) == 'string' then + return val ~= '' and val or nil + end + -- Handle Pandoc objects + if pandoc and pandoc.utils and pandoc.utils.stringify then + local str = pandoc.utils.stringify(val) + return str ~= '' and str or nil + end + local str = tostring(val) + return str ~= '' and str or nil +end + +--- Escape special LaTeX characters in text. +--- @param text string The text to escape +--- @return string The escaped text safe for LaTeX +function M.escape_latex(text) + text = string.gsub(text, "\\", "\\textbackslash{}") + text = string.gsub(text, "%{", "\\{") + text = string.gsub(text, "%}", "\\}") + text = string.gsub(text, "%$", "\\$") + text = string.gsub(text, "%&", "\\&") + text = string.gsub(text, "%%", "\\%%") + text = string.gsub(text, "%#", "\\#") + text = string.gsub(text, "%^", "\\textasciicircum{}") + text = string.gsub(text, "%_", "\\_") + text = string.gsub(text, "~", "\\textasciitilde{}") + return text +end + +--- Escape special Typst characters in text. +--- @param text string The text to escape +--- @return string The escaped text safe for Typst +function M.escape_typst(text) + text = string.gsub(text, "%#", "\\#") + return text +end + +--- Escape special Lua pattern characters for use in string.gsub. +--- @param text string The text containing characters to escape +--- @return string The escaped text safe for Lua patterns +function M.escape_lua_pattern(text) + text = string.gsub(text, "%%", "%%%%") + text = string.gsub(text, "%^", "%%^") + text = string.gsub(text, "%$", "%%$") + text = string.gsub(text, "%(", "%%(") + text = string.gsub(text, "%)", "%%)") + text = string.gsub(text, "%.", "%%.") + text = string.gsub(text, "%[", "%%[") + text = string.gsub(text, "%]", "%%]") + text = string.gsub(text, "%*", "%%*") + text = string.gsub(text, "%+", "%%+") + text = string.gsub(text, "%-", "%%-") + text = string.gsub(text, "%?", "%%?") + return text +end + +--- Escape special HTML characters in text. +--- Escapes &, <, >, ", and ' to prevent XSS and ensure valid HTML. +--- @param text string The text to escape +--- @return string Escaped text safe for use in HTML +--- @usage local escaped = M.escape_html('Hello ') +function M.escape_html(text) + if text == nil then return '' end + if type(text) ~= 'string' then text = tostring(text) end + local result = text + :gsub('&', '&') + :gsub('<', '<') + :gsub('>', '>') + :gsub('"', '"') + :gsub("'", ''') + return result +end + +--- Escape special HTML attribute characters. +--- Escapes characters that could break attribute values. +--- @param value string The attribute value to escape +--- @return string Escaped value safe for use in HTML attributes +--- @usage local escaped = M.escape_attribute('Hello "World"') +function M.escape_attribute(value) + if value == nil then return '' end + if type(value) ~= 'string' then value = tostring(value) end + local result = value + :gsub('&', '&') + :gsub('"', '"') + :gsub('<', '<') + :gsub('>', '>') + return result +end + +--- Escape text for different formats. +--- @param text string The text to escape +--- @param format string The format to escape for (e.g., "latex", "typst", "lua") +--- @return string The escaped text +function M.escape_text(text, format) + local escape_functions = { + latex = M.escape_latex, + typst = M.escape_typst, + lua = M.escape_lua_pattern + } + + local escape = escape_functions[format] + if escape then + return escape(text) + else + error("Unsupported escape format: " .. format) + end +end + +--- Converts a string to a valid HTML id by lowercasing and replacing spaces. +--- @param text string The text to convert +--- @return string The HTML id +function M.ascii_id(text) + local id = text:lower():gsub("[^a-z0-9 ]", ""):gsub(" +", "-") + return id +end + +-- ============================================================================ +-- METADATA UTILITIES +-- ============================================================================ + +--- Get configuration from extensions.mcanouil namespace. +--- @param meta table Document metadata +--- @param key string The key to retrieve +--- @return any The value or nil +--- @usage local value = M.get_mcanouil_config(meta, 'section-outline') +function M.get_mcanouil_config(meta, key) + local mcanouil_ext = meta.extensions and meta.extensions.mcanouil + if not mcanouil_ext then return nil end + return mcanouil_ext[key] +end + +--- Get a section config table from extensions.mcanouil.{section}. +--- @param meta table Document metadata +--- @param section string The section name (e.g., 'code-window', 'typst-markdown') +--- @return table|nil The section config table or nil +--- @usage local config = M.get_mcanouil_section(meta, 'code-window') +function M.get_mcanouil_section(meta, section) + local mcanouil_ext = meta.extensions and meta.extensions.mcanouil + if not mcanouil_ext then return nil end + return mcanouil_ext[section] +end + +--- Extract metadata value from document meta using nested structure. +--- Supports the extensions.{extension-name}.{key} pattern. +--- @param meta table The document metadata table +--- @param extension_name string The extension name (e.g., "github", "iconify") +--- @param key string The metadata key to retrieve +--- @return string|nil The metadata value as a string, or nil if not found +--- @usage local repo = M.get_metadata_value(meta, "github", "repository-name") +function M.get_metadata_value(meta, extension_name, key) + if meta['extensions'] and meta['extensions'][extension_name] and meta['extensions'][extension_name][key] then + return M.stringify(meta['extensions'][extension_name][key]) + end + return nil +end + +--- Check for deprecated top-level configuration and emit warning +--- @param meta table The document metadata table +--- @param extension_name string The extension name +--- @param key string|nil The configuration key being accessed (nil to check entire extension config) +--- @param deprecation_warning_shown boolean Flag to track if warning has been shown +--- @return any|nil The value from deprecated config, or nil if not found +--- @return boolean Updated deprecation warning flag +function M.check_deprecated_config(meta, extension_name, key, deprecation_warning_shown) + -- Handle array-based configuration (when key is nil) + if key == nil then + if not M.is_empty(meta[extension_name]) then + if not deprecation_warning_shown then + M.log_warning( + extension_name, + 'Top-level "' .. extension_name .. '" configuration is deprecated. ' .. + 'Please use:\n' .. + 'extensions:\n' .. + ' ' .. extension_name .. ':\n' .. + ' - (configuration array)' + ) + deprecation_warning_shown = true + end + return meta[extension_name], deprecation_warning_shown + end + return nil, deprecation_warning_shown + end + + -- Handle key-value configuration (original behaviour) + if not M.is_empty(meta[extension_name]) and not M.is_empty(meta[extension_name][key]) then + if not deprecation_warning_shown then + M.log_warning( + extension_name, + 'Top-level "' .. extension_name .. '" configuration is deprecated. ' .. + 'Please use:\n' .. + 'extensions:\n' .. + ' ' .. extension_name .. ':\n' .. + ' ' .. key .. ': value' + ) + deprecation_warning_shown = true + end + return M.stringify(meta[extension_name][key]), deprecation_warning_shown + end + return nil, deprecation_warning_shown +end + +-- ============================================================================ +-- PANDOC/QUARTO FORMAT UTILITIES +-- ============================================================================ + +--- Create a Pandoc Link element +--- @param text string|nil The link text +--- @param uri string|nil The URI to link to +--- @return pandoc.Link|nil A Pandoc Link element or nil if text or uri is empty +function M.create_link(text, uri) + if not M.is_empty(uri) and not M.is_empty(text) then + return pandoc.Link({ pandoc.Str(text --[[@as string]]) }, uri --[[@as string]]) + end + return nil +end + +--- Helper to build Pandoc attributes +--- @param id string|nil Element ID +--- @param classes table|nil List of CSS classes +--- @param attributes table|nil Key-value attributes +--- @return pandoc.Attr Pandoc Attr object +function M.attr(id, classes, attributes) + return pandoc.Attr(id or '', classes or {}, attributes or {}) +end + +--- Check if a class list contains a specific class name +--- @param classes table|nil List of CSS classes +--- @param name string The class name to search for +--- @return boolean True if the class is found, false otherwise +function M.has_class(classes, name) + if not classes then return false end + for _, cls in ipairs(classes) do + if cls == name then return true end + end + return false +end + +--- Add a class to the class list if it doesn't already exist +--- @param classes table List of CSS classes +--- @param name string The class name to add +function M.add_class(classes, name) + if not M.has_class(classes, name) then + table.insert(classes, name) + end +end + +--- Retrieve the current Quarto output format. +--- @return string The output format ("pptx", "html", "latex", "typst", "docx", or "unknown") +--- @return string The language of the output format +function M.get_quarto_format() + if quarto.doc.is_format("html:js") then + return "html", "html" + elseif quarto.doc.is_format("latex") then + return "latex", "latex" + elseif quarto.doc.is_format("typst") then + return "typst", "typst" + elseif quarto.doc.is_format("docx") then + return "docx", "openxml" + elseif quarto.doc.is_format("pptx") then + return "pptx", "openxml" + else + return "unknown", "unknown" + end +end + +-- ============================================================================ +-- OBJECT/TABLE UTILITIES +-- ============================================================================ + +--- Check if an object (including tables and lists) is empty or nil +--- @param obj any The object to check +--- @return boolean true if the object is nil, empty string, or empty table/list +function M.is_object_empty(obj) + local function length(x) + local count = 0 + if x ~= nil then + for _ in pairs(x) do + count = count + 1 + end + end + return count + end + if pandoc.utils.type(obj) == "table" or pandoc.utils.type(obj) == "List" then + return obj == nil or obj == '' or length(obj) == 0 + else + return obj == nil or obj == '' + end +end + +--- Check if an object is a simple type (string, number, or boolean) +--- @param obj any The object to check +--- @return boolean true if the object is a string, number, or boolean +function M.is_type_simple(obj) + return pandoc.utils.type(obj) == "string" or pandoc.utils.type(obj) == "number" or pandoc.utils.type(obj) == "boolean" +end + +--- Check if an object is a function or userdata +--- @param obj any The object to check +--- @return boolean true if the object is a function or userdata +function M.is_function_userdata(obj) + return pandoc.utils.type(obj) == "function" or pandoc.utils.type(obj) == "userdata" +end + +--- Get nested value from object using field path +--- @param fields table Array of field names to traverse +--- @param obj table The object to extract value from +--- @return any The value at the nested path +--- @usage local val = M.get_value({"a", "b", "c"}, obj) +function M.get_value(fields, obj) + local value = obj + for _, field in ipairs(fields) do + value = value[field] + end + return value +end + +--- Convert Pandoc AttributeList to plain table for easier processing. +--- @param element table Element with attributes field (Div, Span, Table, Image) +--- @return table Plain table with attribute key-value pairs +function M.attributes_to_table(element) + local attrs = {} + for k, v in pairs(element.attributes) do + attrs[k] = v + end + return attrs +end + +-- ============================================================================ +-- HTML RAW GENERATION UTILITIES +-- ============================================================================ + +--- Generates a raw HTML header element. +--- @param level integer The header level (e.g., 2 for

) +--- @param text string|nil The header text +--- @param id string The id attribute for the header +--- @param classes table List of classes for the header +--- @param attributes table|nil Additional HTML attributes +--- @return string Raw HTML string for the header +function M.raw_header(level, text, id, classes, attributes) + local attr_str = '' + if id and id ~= '' then + attr_str = attr_str .. ' id="' .. M.escape_attribute(id) .. '"' + end + if classes and #classes > 0 then + local escaped_classes = {} + for i, cls in ipairs(classes) do + escaped_classes[i] = M.escape_attribute(cls) + end + attr_str = attr_str .. ' class="' .. table.concat(escaped_classes, ' ') .. '"' + end + if attributes then + for k, v in pairs(attributes) do + attr_str = attr_str .. ' ' .. M.escape_attribute(k) .. '="' .. M.escape_attribute(v) .. '"' + end + end + return string.format('%s', level, attr_str, M.escape_html(text or ''), level) +end + +-- ============================================================================ +-- HTML DEPENDENCY UTILITIES +-- ============================================================================ + +--- Managed HTML dependency tracker +--- Tracks which dependencies have been added to prevent duplication +--- @type table +local dependency_tracker = {} + +--- Ensure HTML dependency is added only once per document. +--- Prevents duplicate dependency injection by tracking dependencies by name. +--- Returns true if dependency was added, false if already present. +--- +--- @param config table Dependency configuration with fields: name (required), version, scripts, stylesheets, head +--- @return boolean True if dependency was added, false if already added +--- @usage M.ensure_html_dependency({name = 'my-lib', version = '1.0.0', scripts = {'lib.js'}}) +function M.ensure_html_dependency(config) + if not config or not config.name then + error("HTML dependency configuration must include a 'name' field") + end + + --- @type string Unique key for this dependency + local dep_key = config.name + + -- Check if already added + if dependency_tracker[dep_key] then + return false + end + + -- Add the dependency + quarto.doc.add_html_dependency(config) + + -- Mark as added + dependency_tracker[dep_key] = true + return true +end + +--- Reset dependency tracker. +--- Useful for testing or when processing multiple independent documents. +--- In normal usage, this should not be called as dependencies persist per document. +--- +--- @return nil +function M.reset_dependencies() + dependency_tracker = {} +end + +-- ============================================================================ +-- ENHANCED METADATA/CONFIGURATION UTILITIES +-- ============================================================================ + +--- Get option value with fallback hierarchy: args → extensions.{extension}.{key} → defaults. +--- Provides a standardised way to read configuration values with multiple fallback levels. +--- Priority: 1. Named arguments (kwargs), 2. Document metadata, 3. Default values. +--- +--- @param spec table Configuration spec with fields: extension (string), key (string), args (table|nil), meta (table|nil), default (any|nil) +--- @return any The resolved option value (type depends on what's stored in config) +--- @usage local duration = M.get_option_with_fallbacks({extension = 'animate', key = 'duration', args = kwargs, meta = meta, default = '3s'}) +function M.get_option_with_fallbacks(spec) + -- Validate required fields + if not spec.extension or not spec.key then + error("Configuration spec must include 'extension' and 'key' fields") + end + + --- @type string The extension name + local extension = spec.extension + --- @type string The configuration key + local key = spec.key + --- @type table|nil Named arguments table + local args = spec.args + --- @type table|nil Document metadata + local meta = spec.meta + --- @type any Default value if not found elsewhere + local default = spec.default + + -- Priority 1: Check named arguments (kwargs) + if args and args[key] then + local arg_value = M.stringify(args[key]) + if not M.is_empty(arg_value) then + return arg_value + end + end + + -- Priority 2: Check metadata extensions.{extension}.{key} + if meta then + local meta_value = M.get_metadata_value(meta, extension, key) + if not M.is_empty(meta_value) then + return meta_value + end + end + + -- Priority 3: Return default value + return default +end + +--- Get multiple option values at once with fallback hierarchy. +--- Batch version of get_option_with_fallbacks for retrieving multiple configuration values. +--- Returns a table mapping each key to its resolved value. +--- +--- @param spec table Configuration spec with fields: extension (string), keys (table), args (table|nil), meta (table|nil), defaults (table|nil) +--- @return table Table mapping each key to its resolved value +--- @usage local opts = M.get_options({extension = 'animate', keys = {'duration', 'delay'}, args = kwargs, meta = meta, defaults = {duration = '3s', delay = '2s'}}) +function M.get_options(spec) + -- Validate required fields + if not spec.extension or not spec.keys then + error("Configuration spec must include 'extension' and 'keys' fields") + end + + --- @type table Result table + local result = {} + + --- @type table Default values table + local defaults = spec.defaults or {} + + -- Get each key using the single-option fallback logic + for _, key in ipairs(spec.keys) do + result[key] = M.get_option_with_fallbacks({ + extension = spec.extension, + key = key, + args = spec.args, + meta = spec.meta, + default = defaults[key] + }) + end + + return result +end + +-- ============================================================================ +-- LOGGING UTILITIES +-- ============================================================================ + +--- Format and log an error message with extension prefix. +--- Provides standardised error messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "external", "lua-env") +--- @param message string The error message to display +--- @usage M.log_error("external", "Could not open file 'example.md'.") +function M.log_error(extension_name, message) + quarto.log.error("[" .. extension_name .. "] " .. message) +end + +--- Format and log a warning message with extension prefix. +--- Provides standardised warning messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "external", "lua-env") +--- @param message string The warning message to display +--- @usage M.log_warning("lua-env", "No variable name provided.") +function M.log_warning(extension_name, message) + quarto.log.warning("[" .. extension_name .. "] " .. message) +end + +--- Format and log an output message with extension prefix. +--- Provides standardised informational messages with consistent formatting across extensions. +--- Format: [extension-name] Message with details. +--- +--- @param extension_name string The name of the extension (e.g., "lua-env") +--- @param message string The informational message to display +--- @usage M.log_output("lua-env", "Exported metadata to: output.json") +function M.log_output(extension_name, message) + quarto.log.output("[" .. extension_name .. "] " .. message) +end + +-- ============================================================================ +-- PATH UTILITIES +-- ============================================================================ + +--- Resolve a path relative to the project directory. +--- If the path starts with `/`, it is treated as relative to the project directory. +--- If `quarto.project.directory` is available, it is prepended to the path. +--- If `quarto.project.directory` is nil, the leading `/` is removed. +--- @param path string The path to resolve (may start with `/`) +--- @return string The resolved path +--- @usage local resolved = M.resolve_project_path("/config.yml") +--- @usage local resolved = M.resolve_project_path("config.yml") +function M.resolve_project_path(path) + if M.is_empty(path) then + return path + end + + if path:sub(1, 1) == "/" then + if quarto.project.directory then + -- Prepend project directory to absolute path + return quarto.project.directory .. path + else + -- Remove leading `/` if no project directory + return path:sub(2) + end + else + return path + end +end + +-- ============================================================================ +-- COLOUR UTILITIES +-- ============================================================================ + +--- Get colour value from attributes table, accepting both British and American spellings. +--- Checks for 'colour' first (British, primary), then falls back to 'color' (American). +--- @param attrs table Attributes table (kwargs or element.attributes) +--- @param default string|nil Default value if neither spelling is found +--- @return string|nil Colour value or default +--- @usage local colour = M.get_colour(kwargs, 'info') +function M.get_colour(attrs, default) + if attrs == nil then + return default + end + local value = attrs.colour or attrs.color + if M.is_empty(value) then + return default + end + return M.stringify(value) +end + +--- Check if a colour value is a custom colour (hex, rgb, hsl, etc.). +--- Used to determine whether to apply a semantic class or inline style. +--- @param colour string|nil The colour value to check +--- @return boolean True if it's a custom colour value +--- @usage local is_custom = M.is_custom_colour('#ff6600') -- returns true +function M.is_custom_colour(colour) + if not colour then return false end + local str = colour:lower() + return str:match('^#') or str:match('^rgb') or str:match('^hsl') +end + +-- ============================================================================ +-- MODULE EXPORT +-- ============================================================================ + +return M diff --git a/_extensions/mcanouil/iconify/_schema.yml b/_extensions/mcanouil/iconify/_schema.yml new file mode 100644 index 0000000..1b62fb5 --- /dev/null +++ b/_extensions/mcanouil/iconify/_schema.yml @@ -0,0 +1,145 @@ +# Schema for the iconify extension + +$schema: https://m.canouil.dev/quarto-wizard/assets/schema/v1/extension-schema.json + +options: + set: + type: string + default: "octicon" + description: "Default icon set to use when not specified in the shortcode (e.g., 'mdi', 'fa-solid')." + size: + type: string + description: "Default icon size as a keyword or CSS value (e.g., 'large', '2x', '1.5em')." + width: + type: string + description: "Default icon width." + height: + type: string + description: "Default icon height." + flip: + type: string + description: "Default flip transformation for icons." + enum: + - horizontal + - vertical + - horizontal,vertical + rotate: + type: string + description: "Default rotation for icons (e.g., '90deg', '180deg', '1')." + style: + type: string + description: "Default additional inline CSS styles applied to all icons." + inline: + type: boolean + default: true + description: "Whether to render icons inline by default." + mode: + type: string + description: "Default rendering mode for icons." + enum: + - svg + - style + - bg + - mask + +# Per-shortcode schemas. +# Describes positional arguments and named attributes for {{< iconify >}} and {{< quarto >}}. +shortcodes: + iconify: + description: "Renders an Iconify icon. Accepts 'set:icon' or 'set icon' syntax." + arguments: + - name: set-or-icon + type: string + required: true + description: "Icon set name, or 'set:icon' combined format." + - name: icon + type: string + description: "Icon name when the first argument is the set name." + attributes: + size: + type: string + description: "Icon size as a keyword or CSS value (e.g., 'large', '2x', '1.5em')." + width: + type: string + description: "Icon width." + height: + type: string + description: "Icon height." + flip: + type: string + description: "Flip transformation for the icon." + enum: + - horizontal + - vertical + - horizontal,vertical + rotate: + type: string + description: "Rotation for the icon (e.g., '90deg', '180deg', '1')." + inline: + type: string + description: "Whether to render the icon inline." + enum: + - "true" + - "false" + mode: + type: string + description: "Rendering mode for the icon." + enum: + - svg + - style + - bg + - mask + style: + type: string + description: "Additional inline CSS styles for the icon." + label: + type: string + description: "Accessible label for the icon (aria-label)." + title: + type: string + description: "Title attribute for the icon tooltip." + quarto: + description: "Renders the Quarto icon with default blue styling from the simple-icons set." + attributes: + size: + type: string + description: "Icon size as a keyword or CSS value." + width: + type: string + description: "Icon width." + height: + type: string + description: "Icon height." + flip: + type: string + description: "Flip transformation for the icon." + enum: + - horizontal + - vertical + - horizontal,vertical + rotate: + type: string + description: "Rotation for the icon (e.g., '90deg', '180deg', '1')." + inline: + type: string + description: "Whether to render the icon inline." + enum: + - "true" + - "false" + mode: + type: string + description: "Rendering mode for the icon." + enum: + - svg + - style + - bg + - mask + style: + type: string + description: "Additional inline CSS styles for the icon." + label: + type: string + description: "Accessible label for the icon." + title: + type: string + description: "Title attribute for the icon tooltip." diff --git a/_extensions/mcanouil/iconify/_snippets.json b/_extensions/mcanouil/iconify/_snippets.json new file mode 100644 index 0000000..e2fba87 --- /dev/null +++ b/_extensions/mcanouil/iconify/_snippets.json @@ -0,0 +1,17 @@ +{ + "Icon": { + "prefix": "iconify", + "body": "{{< iconify ${1:fluent-emoji}:${2:exploding-head} >}}", + "description": "Insert an Iconify icon using set:icon syntax" + }, + "Icon with size": { + "prefix": "iconify-size", + "body": "{{< iconify ${1:fluent-emoji}:${2:exploding-head} size=${3:1em} >}}", + "description": "Insert an Iconify icon with a size" + }, + "Icon with style": { + "prefix": "iconify-style", + "body": "{{< iconify ${1:simple-icons}:${2:quarto} style=\"color:${3:#74aadb};\" >}}", + "description": "Insert an Iconify icon with custom CSS style" + } +} diff --git a/_extensions/mcanouil/iconify/iconify-icon.min.js b/_extensions/mcanouil/iconify/iconify-icon.min.js new file mode 100644 index 0000000..2b9c109 --- /dev/null +++ b/_extensions/mcanouil/iconify/iconify-icon.min.js @@ -0,0 +1,13 @@ +/** +* (c) Iconify +* +* For the full copyright and license information, please view the license.txt +* files at https://github.com/iconify/iconify +* +* Licensed under MIT. +* Source: https://github.com/iconify/code/tree/gh-pages/iconify-icon +* +* @license MIT +* @version 3.0.2 +*/ +!function(){"use strict";const t=Object.freeze({left:0,top:0,width:16,height:16}),e=Object.freeze({rotate:0,vFlip:!1,hFlip:!1}),n=Object.freeze({...t,...e}),i=Object.freeze({...n,body:"",hidden:!1}),r=Object.freeze({width:null,height:null}),o=Object.freeze({...r,...e});const s=/[\s,]+/;const c={...o,preserveAspectRatio:""};function a(t){const e={...c},n=(e,n)=>t.getAttribute(e)||n;var i;return e.width=n("width",null),e.height=n("height",null),e.rotate=function(t,e=0){const n=t.replace(/^-?[0-9.]*/,"");function i(t){for(;t<0;)t+=4;return t%4}if(""===n){const e=parseInt(t);return isNaN(e)?0:i(e)}if(n!==t){let e=0;switch(n){case"%":e=25;break;case"deg":e=90}if(e){let r=parseFloat(t.slice(0,t.length-n.length));return isNaN(r)?0:(r/=e,r%1==0?i(r):0)}}return e}(n("rotate","")),i=e,n("flip","").split(s).forEach(t=>{switch(t.trim()){case"horizontal":i.hFlip=!0;break;case"vertical":i.vFlip=!0}}),e.preserveAspectRatio=n("preserveAspectRatio",n("preserveaspectratio","")),e}const u=/^[a-z0-9]+(-[a-z0-9]+)*$/,l=(t,e,n,i="")=>{const r=t.split(":");if("@"===t.slice(0,1)){if(r.length<2||r.length>3)return null;i=r.shift().slice(1)}if(r.length>3||!r.length)return null;if(r.length>1){const t=r.pop(),n=r.pop(),o={provider:r.length>0?r[0]:i,prefix:n,name:t};return e&&!f(o)?null:o}const o=r[0],s=o.split("-");if(s.length>1){const t={provider:i,prefix:s.shift(),name:s.join("-")};return e&&!f(t)?null:t}if(n&&""===i){const t={provider:i,prefix:"",name:o};return e&&!f(t,n)?null:t}return null},f=(t,e)=>!!t&&!(!(e&&""===t.prefix||t.prefix)||!t.name);function d(t,n){const r=function(t,e){const n={};!t.hFlip!=!e.hFlip&&(n.hFlip=!0),!t.vFlip!=!e.vFlip&&(n.vFlip=!0);const i=((t.rotate||0)+(e.rotate||0))%4;return i&&(n.rotate=i),n}(t,n);for(const o in i)o in e?o in t&&!(o in r)&&(r[o]=e[o]):o in n?r[o]=n[o]:o in t&&(r[o]=t[o]);return r}function h(t,e,n){const i=t.icons,r=t.aliases||Object.create(null);let o={};function s(t){o=d(i[t]||r[t],o)}return s(e),n.forEach(s),d(t,o)}function p(t,e){const n=[];if("object"!=typeof t||"object"!=typeof t.icons)return n;t.not_found instanceof Array&&t.not_found.forEach(t=>{e(t,null),n.push(t)});const i=function(t){const e=t.icons,n=t.aliases||Object.create(null),i=Object.create(null);return Object.keys(e).concat(Object.keys(n)).forEach(function t(r){if(e[r])return i[r]=[];if(!(r in i)){i[r]=null;const e=n[r]&&n[r].parent,o=e&&t(e);o&&(i[r]=[e].concat(o))}return i[r]}),i}(t);for(const r in i){const o=i[r];o&&(e(r,h(t,r,o)),n.push(r))}return n}const g={provider:"",aliases:{},not_found:{},...t};function b(t,e){for(const n in e)if(n in t&&typeof t[n]!=typeof e[n])return!1;return!0}function v(t){if("object"!=typeof t||null===t)return null;const e=t;if("string"!=typeof e.prefix||!t.icons||"object"!=typeof t.icons)return null;if(!b(t,g))return null;const n=e.icons;for(const t in n){const e=n[t];if(!t||"string"!=typeof e.body||!b(e,i))return null}const r=e.aliases||Object.create(null);for(const t in r){const e=r[t],o=e.parent;if(!t||"string"!=typeof o||!n[o]&&!r[o]||!b(e,i))return null}return e}const m=Object.create(null);function y(t,e){const n=m[t]||(m[t]=Object.create(null));return n[e]||(n[e]=function(t,e){return{provider:t,prefix:e,icons:Object.create(null),missing:new Set}}(t,e))}function x(t,e){return v(e)?p(e,(e,n)=>{n?t.icons[e]=n:t.missing.add(e)}):[]}function _(t,e){let n=[];return("string"==typeof t?[t]:Object.keys(m)).forEach(t=>{("string"==typeof t&&"string"==typeof e?[e]:Object.keys(m[t]||{})).forEach(e=>{const i=y(t,e);n=n.concat(Object.keys(i.icons).map(n=>(""!==t?"@"+t+":":"")+e+":"+n))})}),n}let w=!1;function k(t){return"boolean"==typeof t&&(w=t),w}function A(t){const e="string"==typeof t?l(t,!0,w):t;if(e){const t=y(e.provider,e.prefix),n=e.name;return t.icons[n]||(t.missing.has(n)?null:void 0)}}function j(t,e){const n=l(t,!0,w);if(!n)return!1;const i=y(n.provider,n.prefix);return e?function(t,e,n){try{if("string"==typeof n.body)return t.icons[e]={...n},!0}catch(t){}return!1}(i,n.name,e):(i.missing.add(n.name),!0)}function O(t,e){if("object"!=typeof t)return!1;if("string"!=typeof e&&(e=t.provider||""),w&&!e&&!t.prefix){let e=!1;return v(t)&&(t.prefix="",p(t,(t,n)=>{j(t,n)&&(e=!0)})),e}const n=t.prefix;return!!f({prefix:n,name:"a"})&&!!x(y(e,n),t)}function C(t){return!!A(t)}function I(t){const e=A(t);return e?{...n,...e}:e}function E(t,e){t.forEach(t=>{const n=t.loaderCallbacks;n&&(t.loaderCallbacks=n.filter(t=>t.id!==e))})}let T=0;const F=Object.create(null);function R(t,e){F[t]=e}function S(t){return F[t]||F[""]}function L(t){let e;if("string"==typeof t.resources)e=[t.resources];else if(e=t.resources,!(e instanceof Array&&e.length))return null;return{resources:e,path:t.path||"/",maxURL:t.maxURL||500,rotate:t.rotate||750,timeout:t.timeout||5e3,random:!0===t.random,index:t.index||0,dataAfterTimeout:!1!==t.dataAfterTimeout}}const P=Object.create(null),M=["https://api.simplesvg.com","https://api.unisvg.com"],N=[];for(;M.length>0;)1===M.length||Math.random()>.5?N.push(M.shift()):N.push(M.pop());function z(t,e){const n=L(e);return null!==n&&(P[t]=n,!0)}function Q(t){return P[t]}function q(){return Object.keys(P)}P[""]=L({resources:["https://api.iconify.design"].concat(N)});const U={resources:[],index:0,timeout:2e3,rotate:750,random:!1,dataAfterTimeout:!1};function D(t,e,n,i){const r=t.resources.length,o=t.random?Math.floor(Math.random()*r):t.index;let s;if(t.random){let e=t.resources.slice(0);for(s=[];e.length>1;){const t=Math.floor(Math.random()*e.length);s.push(e[t]),e=e.slice(0,t).concat(e.slice(t+1))}s=s.concat(e)}else s=t.resources.slice(o).concat(t.resources.slice(0,o));const c=Date.now();let a,u="pending",l=0,f=null,d=[],h=[];function p(){f&&(clearTimeout(f),f=null)}function g(){"pending"===u&&(u="aborted"),p(),d.forEach(t=>{"pending"===t.status&&(t.status="aborted")}),d=[]}function b(t,e){e&&(h=[]),"function"==typeof t&&h.push(t)}function v(){u="failed",h.forEach(t=>{t(void 0,a)})}function m(){d.forEach(t=>{"pending"===t.status&&(t.status="aborted")}),d=[]}function y(){if("pending"!==u)return;p();const i=s.shift();if(void 0===i)return d.length?void(f=setTimeout(()=>{p(),"pending"===u&&(m(),v())},t.timeout)):void v();const r={status:"pending",resource:i,callback:(e,n)=>{!function(e,n,i){const r="success"!==n;switch(d=d.filter(t=>t!==e),u){case"pending":break;case"failed":if(r||!t.dataAfterTimeout)return;break;default:return}if("abort"===n)return a=i,void v();if(r)return a=i,void(d.length||(s.length?y():v()));if(p(),m(),!t.random){const n=t.resources.indexOf(e.resource);-1!==n&&n!==t.index&&(t.index=n)}u="completed",h.forEach(t=>{t(i)})}(r,e,n)}};d.push(r),l++,f=setTimeout(y,t.rotate),n(i,e,r.callback)}return"function"==typeof i&&h.push(i),setTimeout(y),function(){return{startTime:c,payload:e,status:u,queriesSent:l,queriesPending:d.length,subscribe:b,abort:g}}}function H(t){const e={...U,...t};let n=[];function i(){n=n.filter(t=>"pending"===t().status)}return{query:function(t,r,o){const s=D(e,t,r,(t,e)=>{i(),o&&o(t,e)});return n.push(s),s},find:function(t){return n.find(e=>t(e))||null},setIndex:t=>{e.index=t},getIndex:()=>e.index,cleanup:i}}function J(){}const $=Object.create(null);function B(t,e,n){let i,r;if("string"==typeof t){const e=S(t);if(!e)return n(void 0,424),J;r=e.send;const o=function(t){if(!$[t]){const e=Q(t);if(!e)return;$[t]={config:e,redundancy:H(e)}}return $[t]}(t);o&&(i=o.redundancy)}else{const e=L(t);if(e){i=H(e);const n=S(t.resources?t.resources[0]:"");n&&(r=n.send)}}return i&&r?i.query(e,r,n)().abort:(n(void 0,424),J)}function G(){}function V(t){t.iconsLoaderFlag||(t.iconsLoaderFlag=!0,setTimeout(()=>{t.iconsLoaderFlag=!1,function(t){t.pendingCallbacksFlag||(t.pendingCallbacksFlag=!0,setTimeout(()=>{t.pendingCallbacksFlag=!1;const e=t.loaderCallbacks?t.loaderCallbacks.slice(0):[];if(!e.length)return;let n=!1;const i=t.provider,r=t.prefix;e.forEach(e=>{const o=e.icons,s=o.pending.length;o.pending=o.pending.filter(e=>{if(e.prefix!==r)return!0;const s=e.name;if(t.icons[s])o.loaded.push({provider:i,prefix:r,name:s});else{if(!t.missing.has(s))return n=!0,!0;o.missing.push({provider:i,prefix:r,name:s})}return!1}),o.pending.length!==s&&(n||E([t],e.id),e.callback(o.loaded.slice(0),o.missing.slice(0),o.pending.slice(0),e.abort))})}))}(t)}))}function K(t,e,n){function i(){const n=t.pendingIcons;e.forEach(e=>{n&&n.delete(e),t.icons[e]||t.missing.add(e)})}if(n&&"object"==typeof n)try{if(!x(t,n).length)return void i()}catch(t){console.error(t)}i(),V(t)}function W(t,e){t instanceof Promise?t.then(t=>{e(t)}).catch(()=>{e(null)}):e(t)}function X(t,e){t.iconsToLoad?t.iconsToLoad=t.iconsToLoad.concat(e).sort():t.iconsToLoad=e,t.iconsQueueFlag||(t.iconsQueueFlag=!0,setTimeout(()=>{t.iconsQueueFlag=!1;const{provider:e,prefix:n}=t,i=t.iconsToLoad;if(delete t.iconsToLoad,!i||!i.length)return;const r=t.loadIcon;if(t.loadIcons&&(i.length>1||!r))return void W(t.loadIcons(i,n,e),e=>{K(t,i,e)});if(r)return void i.forEach(i=>{W(r(i,n,e),e=>{K(t,[i],e?{prefix:n,icons:{[i]:e}}:null)})});const{valid:o,invalid:s}=function(t){const e=[],n=[];return t.forEach(t=>{(t.match(u)?e:n).push(t)}),{valid:e,invalid:n}}(i);if(s.length&&K(t,s,null),!o.length)return;const c=n.match(u)?S(e):null;c?c.prepare(e,n,o).forEach(n=>{B(e,n,e=>{K(t,n.icons,e)})}):K(t,o,null)}))}const Y=(t,e)=>{const n=function(t){const e={loaded:[],missing:[],pending:[]},n=Object.create(null);t.sort((t,e)=>t.provider!==e.provider?t.provider.localeCompare(e.provider):t.prefix!==e.prefix?t.prefix.localeCompare(e.prefix):t.name.localeCompare(e.name));let i={provider:"",prefix:"",name:""};return t.forEach(t=>{if(i.name===t.name&&i.prefix===t.prefix&&i.provider===t.provider)return;i=t;const r=t.provider,o=t.prefix,s=t.name,c=n[r]||(n[r]=Object.create(null)),a=c[o]||(c[o]=y(r,o));let u;u=s in a.icons?e.loaded:""===o||a.missing.has(s)?e.missing:e.pending;const l={provider:r,prefix:o,name:s};u.push(l)}),e}(function(t,e=!0,n=!1){const i=[];return t.forEach(t=>{const r="string"==typeof t?l(t,e,n):t;r&&i.push(r)}),i}(t,!0,k()));if(!n.pending.length){let t=!0;return e&&setTimeout(()=>{t&&e(n.loaded,n.missing,n.pending,G)}),()=>{t=!1}}const i=Object.create(null),r=[];let o,s;return n.pending.forEach(t=>{const{provider:e,prefix:n}=t;if(n===s&&e===o)return;o=e,s=n,r.push(y(e,n));const c=i[e]||(i[e]=Object.create(null));c[n]||(c[n]=[])}),n.pending.forEach(t=>{const{provider:e,prefix:n,name:r}=t,o=y(e,n),s=o.pendingIcons||(o.pendingIcons=new Set);s.has(r)||(s.add(r),i[e][n].push(r))}),r.forEach(t=>{const e=i[t.provider][t.prefix];e.length&&X(t,e)}),e?function(t,e,n){const i=T++,r=E.bind(null,n,i);if(!e.pending.length)return r;const o={id:i,icons:e,callback:t,abort:r};return n.forEach(t=>{(t.loaderCallbacks||(t.loaderCallbacks=[])).push(o)}),r}(e,n,r):G},Z=t=>new Promise((e,i)=>{const r="string"==typeof t?l(t,!0):t;r?Y([r||t],o=>{if(o.length&&r){const t=A(r);if(t)return void e({...n,...t})}i(t)}):i(t)});function tt(t){try{const e="string"==typeof t?JSON.parse(t):t;if("string"==typeof e.body)return{...e}}catch(t){}}let et=!1;try{et=0===navigator.vendor.indexOf("Apple")}catch(t){}const nt=/(-?[0-9.]*[0-9]+[0-9.]*)/g,it=/^-?[0-9.]*[0-9]+[0-9.]*$/g;function rt(t,e,n){if(1===e)return t;if(n=n||100,"number"==typeof t)return Math.ceil(t*e*n)/n;if("string"!=typeof t)return t;const i=t.split(nt);if(null===i||!i.length)return t;const r=[];let o=i.shift(),s=it.test(o);for(;;){if(s){const t=parseFloat(o);isNaN(t)?r.push(o):r.push(Math.ceil(t*e*n)/n)}else r.push(o);if(o=i.shift(),void 0===o)return r.join("");s=!s}}function ot(t,e){const i={...n,...t},r={...o,...e},s={left:i.left,top:i.top,width:i.width,height:i.height};let c=i.body;[i,r].forEach(t=>{const e=[],n=t.hFlip,i=t.vFlip;let r,o=t.rotate;switch(n?i?o+=2:(e.push("translate("+(s.width+s.left).toString()+" "+(0-s.top).toString()+")"),e.push("scale(-1 1)"),s.top=s.left=0):i&&(e.push("translate("+(0-s.left).toString()+" "+(s.height+s.top).toString()+")"),e.push("scale(1 -1)"),s.top=s.left=0),o<0&&(o-=4*Math.floor(o/4)),o%=4,o){case 1:r=s.height/2+s.top,e.unshift("rotate(90 "+r.toString()+" "+r.toString()+")");break;case 2:e.unshift("rotate(180 "+(s.width/2+s.left).toString()+" "+(s.height/2+s.top).toString()+")");break;case 3:r=s.width/2+s.left,e.unshift("rotate(-90 "+r.toString()+" "+r.toString()+")")}o%2==1&&(s.left!==s.top&&(r=s.left,s.left=s.top,s.top=r),s.width!==s.height&&(r=s.width,s.width=s.height,s.height=r)),e.length&&(c=function(t,e,n){const i=function(t,e="defs"){let n="";const i=t.indexOf("<"+e);for(;i>=0;){const r=t.indexOf(">",i),o=t.indexOf("",o);if(-1===s)break;n+=t.slice(r+1,o).trim(),t=t.slice(0,i).trim()+t.slice(s+1)}return{defs:n,content:t}}(t);return r=i.defs,o=e+i.content+n,r?""+r+""+o:o;var r,o}(c,'',""))});const a=r.width,u=r.height,l=s.width,f=s.height;let d,h;null===a?(h=null===u?"1em":"auto"===u?f:u,d=rt(h,l/f)):(d="auto"===a?l:a,h=null===u?rt(d,f/l):"auto"===u?f:u);const p={},g=(t,e)=>{(t=>"unset"===t||"undefined"===t||"none"===t)(e)||(p[t]=e.toString())};g("width",d),g("height",h);const b=[s.left,s.top,l,f];return p.viewBox=b.join(" "),{attributes:p,viewBox:b,body:c}}function st(t,e){let n=-1===t.indexOf("xlink:")?"":' xmlns:xlink="http://www.w3.org/1999/xlink"';for(const t in e)n+=" "+t+'="'+e[t]+'"';return'"+t+""}function ct(t){return'url("'+function(t){return"data:image/svg+xml,"+function(t){return t.replace(/"/g,"'").replace(/%/g,"%25").replace(/#/g,"%23").replace(//g,"%3E").replace(/\s+/g," ")}(t)}(t)+'")'}let at=(()=>{let t;try{if(t=fetch,"function"==typeof t)return t}catch(t){}})();function ut(t){at=t}function lt(){return at}const ft={prepare:(t,e,n)=>{const i=[],r=function(t,e){const n=Q(t);if(!n)return 0;let i;if(n.maxURL){let t=0;n.resources.forEach(e=>{const n=e;t=Math.max(t,n.length)});const r=e+".json?icons=";i=n.maxURL-t-n.path.length-r.length}else i=0;return i}(t,e),o="icons";let s={type:o,provider:t,prefix:e,icons:[]},c=0;return n.forEach((n,a)=>{c+=n.length+1,c>=r&&a>0&&(i.push(s),s={type:o,provider:t,prefix:e,icons:[]},c=n.length),s.icons.push(n)}),i.push(s),i},send:(t,e,n)=>{if(!at)return void n("abort",424);let i=function(t){if("string"==typeof t){const e=Q(t);if(e)return e.path}return"/"}(e.provider);switch(e.type){case"icons":{const t=e.prefix,n=e.icons.join(",");i+=t+".json?"+new URLSearchParams({icons:n}).toString();break}case"custom":{const t=e.uri;i+="/"===t.slice(0,1)?t.slice(1):t;break}default:return void n("abort",400)}let r=503;at(t+i).then(t=>{const e=t.status;if(200===e)return r=501,t.json();setTimeout(()=>{n(function(t){return 404===t}(e)?"abort":"next",e)})}).then(t=>{"object"==typeof t&&null!==t?setTimeout(()=>{n("success",t)}):setTimeout(()=>{404===t?n("abort",t):n("next",r)})}).catch(()=>{n("next",r)})}};function dt(t,e,n){y(n||"",e).loadIcons=t}function ht(t,e,n){y(n||"",e).loadIcon=t}const pt="data-style";let gt="";function bt(t){gt=t}function vt(t,e){let n=Array.from(t.childNodes).find(t=>t.hasAttribute&&t.hasAttribute(pt));n||(n=document.createElement("style"),n.setAttribute(pt,pt),t.appendChild(n)),n.textContent=":host{display:inline-block;vertical-align:"+(e?"-0.125em":"0")+"}span,svg{display:block;margin:auto}"+gt}const mt={"background-color":"currentColor"},yt={"background-color":"transparent"},xt={image:"var(--svg)",repeat:"no-repeat",size:"100% 100%"},_t={"-webkit-mask":mt,mask:mt,background:yt};for(const t in _t){const e=_t[t];for(const n in xt)e[t+"-"+n]=xt[n]}function wt(t){return t?t+(t.match(/^[-0-9.]+$/)?"px":""):"inherit"}let kt;function At(t){return void 0===kt&&function(){try{kt=window.trustedTypes.createPolicy("iconify",{createHTML:t=>t})}catch(t){kt=null}}(),kt?kt.createHTML(t):t}function jt(t){return Array.from(t.childNodes).find(t=>{const e=t.tagName&&t.tagName.toUpperCase();return"SPAN"===e||"SVG"===e})}function Ot(t,e){const i=e.icon.data,r=e.customisations,o=ot(i,r);r.preserveAspectRatio&&(o.attributes.preserveAspectRatio=r.preserveAspectRatio);const s=e.renderedMode;let c;if("svg"===s)c=function(t){const e=document.createElement("span"),n=t.attributes;let i="";n.width||(i="width: inherit;"),n.height||(i+="height: inherit;"),i&&(n.style=i);const r=st(t.body,n);return e.innerHTML=At(r),e.firstChild}(o);else c=function(t,e,n){const i=document.createElement("span");let r=t.body;-1!==r.indexOf("{this._check()}))}_check(){if(!this._checkQueued)return;this._checkQueued=!1;const t=this._state,e=this.getAttribute("icon");if(e!==t.icon.value)return void this._iconChanged(e);if(!t.rendered||!this._visible)return;const n=this.getAttribute("mode"),i=a(this);t.attrMode===n&&!function(t,e){for(const n in c)if(t[n]!==e[n])return!0;return!1}(t.customisations,i)&&jt(this._shadowRoot)||this._renderIcon(t.icon,i,n)}_iconChanged(t){const e=function(t,e){if("object"==typeof t)return{data:tt(t),value:t};if("string"!=typeof t)return{value:t};if(t.includes("{")){const e=tt(t);if(e)return{data:e,value:t}}const n=l(t,!0,!0);if(!n)return{value:t};const i=A(n);if(void 0!==i||!n.prefix)return{value:t,name:n,data:i};const r=Y([n],()=>e(t,n,A(n)));return{value:t,name:n,loading:r}}(t,(t,e,n)=>{const i=this._state;if(i.rendered||this.getAttribute("icon")!==t)return;const r={value:t,name:e,data:n};r.data?this._gotIconData(r):i.icon=r});e.data?this._gotIconData(e):this._state=Ct(e,this._state.inline,this._state)}_forceRender(){if(!this._visible){const t=jt(this._shadowRoot);return void(t&&this._shadowRoot.removeChild(t))}this._queueCheck()}_gotIconData(t){this._checkQueued=!1,this._renderIcon(t,a(this),this.getAttribute("mode"))}_renderIcon(t,e,n){const i=function(t,e){switch(e){case"svg":case"bg":case"mask":return e}return"style"===e||!et&&-1!==t.indexOf("{const e=t.some(t=>t.isIntersecting);e!==this._visible&&(this._visible=e,this._forceRender())}),this._observer.observe(this)}catch(t){if(this._observer){try{this._observer.disconnect()}catch(t){}this._observer=null}}}stopObserver(){this._observer&&(this._observer.disconnect(),this._observer=null,this._visible=!0,this._connected&&this._forceRender())}};r.forEach(t=>{t in o.prototype||Object.defineProperty(o.prototype,t,{get:function(){return this.getAttribute(t)},set:function(e){null!==e?this.setAttribute(t,e):this.removeAttribute(t)}})});const s=function(){let t;R("",ft),k(!0);try{t=window}catch(t){}if(t){if(void 0!==t.IconifyPreload){const e=t.IconifyPreload,n="Invalid IconifyPreload syntax.";"object"==typeof e&&null!==e&&(e instanceof Array?e:[e]).forEach(t=>{try{("object"!=typeof t||null===t||t instanceof Array||"object"!=typeof t.icons||"string"!=typeof t.prefix||!O(t))&&console.error(n)}catch(t){console.error(n)}})}if(void 0!==t.IconifyProviders){const e=t.IconifyProviders;if("object"==typeof e&&null!==e)for(const t in e){const n="IconifyProviders["+t+"] is invalid.";try{const i=e[t];if("object"!=typeof i||!i||void 0===i.resources)continue;z(t,i)||console.error(n)}catch(t){console.error(n)}}}}return{iconLoaded:C,getIcon:I,listIcons:_,addIcon:j,addCollection:O,calculateSize:rt,buildIcon:ot,iconToHTML:st,svgToURL:ct,loadIcons:Y,loadIcon:Z,addAPIProvider:z,setCustomIconLoader:ht,setCustomIconsLoader:dt,appendCustomStyle:bt,_api:{getAPIConfig:Q,setAPIModule:R,sendAPIQuery:B,setFetch:ut,getFetch:lt,listAPIProviders:q}}}();for(const t in s)o[t]=o.prototype[t]=s[t];e.define(t,o)}()}(); diff --git a/_extensions/mcanouil/iconify/iconify.lua b/_extensions/mcanouil/iconify/iconify.lua new file mode 100644 index 0000000..2051402 --- /dev/null +++ b/_extensions/mcanouil/iconify/iconify.lua @@ -0,0 +1,265 @@ +--- @module iconify +--- @license MIT +--- @copyright 2026 Mickaël Canouil +--- @author Mickaël Canouil + +--- Extension name constant +local EXTENSION_NAME = "iconify" + +--- Load utils module +local utils = require(quarto.utils.resolve_path("_modules/utils.lua"):gsub("%.lua$", "")) + +--- Flag to track if deprecation warning has been shown +--- @type boolean +local deprecation_warning_shown = false + +--- Ensure Iconify HTML dependencies are included. +--- @return nil +local function ensure_html_deps() + quarto.doc.add_html_dependency({ + name = 'iconify', + version = '3.0.0', + scripts = { 'iconify-icon.min.js' } + }) +end + +--- Check for deprecated top-level iconify configuration and emit warning. +--- @param meta table Document metadata table +--- @param key string The configuration key being accessed +--- @return string|nil The value from deprecated config, or nil if not found +local function check_deprecated_config(meta, key) + local value + value, deprecation_warning_shown = utils.check_deprecated_config(meta, 'iconify', key, deprecation_warning_shown) + return value +end + +--- Validate and convert size keyword to CSS font-size. +--- @param size string|nil +--- @return string +local function is_valid_size(size) + if utils.is_empty(size) then + return '' + end + --- @type table + local size_table = { + ['tiny'] = '0.5em', + ['scriptsize'] = '0.7em', + ['footnotesize'] = '0.8em', + ['small'] = '0.9em', + ['normalsize'] = '1em', + ['large'] = '1.2em', + ['Large'] = '1.5em', + ['LARGE'] = '1.75em', + ['huge'] = '2em', + ['Huge'] = '2.5em', + ['1x'] = '1em', + ['2x'] = '2em', + ['3x'] = '3em', + ['4x'] = '4em', + ['5x'] = '5em', + ['6x'] = '6em', + ['7x'] = '7em', + ['8x'] = '8em', + ['9x'] = '9em', + ['10x'] = '10em', + ['2xs'] = '0.625em', + ['xs'] = '0.75em', + ['sm'] = '0.875em', + ['lg'] = '1.25em', + ['xl'] = '1.5em', + ['2xl'] = '2em' + } + for key, value in pairs(size_table) do + if key == size then + return 'font-size: ' .. value .. ';' + end + end + return 'font-size: ' .. size .. ';' +end + +--- Get iconify option from arguments or metadata. +--- @param x string The option name to retrieve +--- @param arg table Arguments table containing options +--- @param meta table Document metadata table +--- @return string The option value as a string +local function get_iconify_options(x, arg, meta) + --- @type string + local arg_value = utils.stringify(arg[x]) + + -- Return argument value if provided + if not utils.is_empty(arg_value) then + return arg_value + end + + -- Check new nested structure: extensions.iconify.x + local meta_value = utils.get_metadata_value(meta, 'iconify', x) + if not utils.is_empty(meta_value) then + return meta_value + end + + -- Check deprecated top-level structure: iconify.x (with warning) + local deprecated_value = check_deprecated_config(meta, x) + if deprecated_value then + return deprecated_value + end + + return arg_value +end + +--- Render an Iconify icon as a Pandoc RawInline for HTML output. +--- @param args table Icon arguments (icon set and name) +--- @param kwargs table Key-value options for the icon +--- @param meta table Document metadata +--- @return any Pandoc RawInline for HTML or Pandoc Null for other formats +local function iconify(args, kwargs, meta) + -- detect html (excluding epub which won't handle fa) + if quarto.doc.is_format('html:js') then + ensure_html_deps() + --- @type string + local icon = utils.stringify(args[1]) + --- @type string + local set = 'octicon' + + -- Check new nested structure for default set + local meta_set = utils.get_metadata_value(meta, 'iconify', 'set') + if not utils.is_empty(meta_set) then + set = meta_set + else + -- Check deprecated top-level structure for default set (with warning) + local deprecated_set = check_deprecated_config(meta, 'set') + if deprecated_set then + set = deprecated_set + end + end + + if #args > 1 and string.find(utils.stringify(args[2]), ':') then + utils.log_warning( + EXTENSION_NAME, + 'Use "set:icon" or "set icon" syntax, not both! ' .. + 'Using "set:icon" syntax and discarding first argument!' + ) + icon = utils.stringify(args[2]) + end + + if string.find(icon, ':') then + set = string.sub(icon, 1, string.find(icon, ':') - 1) + icon = string.sub(icon, string.find(icon, ':') + 1) + elseif #args > 1 then + set = icon + icon = utils.stringify(args[2]) + end + + --- @type string + local attributes = ' icon="' .. set .. ':' .. icon .. '"' + --- @type string + local default_label = 'Icon ' .. icon .. ' from ' .. set .. ' Iconify.design set.' + + --- @type string + local size = is_valid_size(get_iconify_options('size', kwargs, meta)) + --- @type string + local style = get_iconify_options('style', kwargs, meta) + + if utils.is_empty(style) and not utils.is_empty(size) then + attributes = attributes .. ' style="' .. size .. '"' + elseif not utils.is_empty(style) and not utils.is_empty(size) then + attributes = attributes .. ' style="' .. style .. ';' .. size .. '"' + elseif not utils.is_empty(style) then + attributes = attributes .. ' style="' .. style .. '"' + end + + --- @type string + local aria_label = utils.stringify(kwargs['label']) + if utils.is_empty(aria_label) then + aria_label = ' aria-label="' .. default_label .. '"' + else + aria_label = ' aria-label="' .. aria_label .. '"' + end + + --- @type string + local title = utils.stringify(kwargs['title']) + if utils.is_empty(title) then + title = ' title="' .. default_label .. '"' + else + title = ' title="' .. title .. '"' + end + + attributes = attributes .. aria_label .. title + + --- @type string + local width = get_iconify_options('width', kwargs, meta) + if not utils.is_empty(width) and utils.is_empty(size) then + attributes = attributes .. ' width="' .. width .. '"' + end + --- @type string + local height = get_iconify_options('height', kwargs, meta) + if not utils.is_empty(height) and utils.is_empty(size) then + attributes = attributes .. ' height="' .. height .. '"' + end + --- @type string + local flip = get_iconify_options('flip', kwargs, meta) + if not utils.is_empty(flip) then + attributes = attributes .. ' flip="' .. flip .. '"' + end + --- @type string + local rotate = get_iconify_options('rotate', kwargs, meta) + if not utils.is_empty(rotate) then + attributes = attributes .. ' rotate="' .. rotate .. '"' + end + + --- @type string + local inline = get_iconify_options('inline', kwargs, meta) + if utils.is_empty(inline) or inline ~= 'false' then + attributes = ' inline ' .. attributes + end + + --- @type string + local mode = get_iconify_options('mode', kwargs, meta) + --- @type table + local valid_modes = { svg = true, style = true, bg = true, mask = true } + if not utils.is_empty(mode) and valid_modes[mode] then + attributes = attributes .. ' mode="' .. mode .. '"' + end + + return pandoc.RawInline( + 'html', + '' + ) + else + return pandoc.Null() + end +end + +--- Render Quarto icon using the iconify function with preset styling. +--- @param args table Icon arguments (ignored as we're using a preset icon) +--- @param kwargs table|nil Key-value options that might override default styling +--- @param meta table Document metadata +--- @return any Pandoc RawInline for HTML or Pandoc Null for other formats +local function iconify_quarto(args, kwargs, meta) + --- @type table + local quarto_args = { 'simple-icons:quarto' } + --- @type table + local quarto_kwargs = kwargs or {} + quarto_kwargs['label'] = 'Quarto icon' + quarto_kwargs['title'] = 'Quarto icon' + --- @type string + local quarto_colour = 'color:#74aadb;' + + if not utils.is_empty(quarto_kwargs['style']) then + --- @type string + local style = utils.stringify(quarto_kwargs['style']) + if string.match(style, 'color:[^;]+;') then + quarto_kwargs['style'] = string.gsub(style, 'color:[^;]+;', quarto_colour) + else + quarto_kwargs['style'] = quarto_colour .. style + end + else + quarto_kwargs['style'] = quarto_colour + end + return iconify(quarto_args, quarto_kwargs, meta) +end + +--- @type table +return { + ['iconify'] = iconify, + ['quarto'] = iconify_quarto +} diff --git a/_freeze/contributing/colors-and-fonts/execute-results/html.json b/_freeze/contributing/colors-and-fonts/execute-results/html.json new file mode 100644 index 0000000..4ba75b1 --- /dev/null +++ b/_freeze/contributing/colors-and-fonts/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "60612fe49dbceaa98f588ba2971331ee", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Colors and Fonts\"\nengine: knitr\n---\n\n```{=html}\n\n```\n\nOur colors and themes are inspired by the delightfully awful 90's color choices \nof the R Foundation, particularly [its logo](https://www.r-project.org/logo/), \nand the Rust Foundation's official [brand guide](https://rustfoundation.org/brand-guide/). \n\n## Colors\n\nWe have defined several utility classes to make using consistent colors easier.\nThese can be used to define background and text colors. They are also used to\ndefine semantic colors, which are applied automatically. \n\n### Background colors\n\n:::::: {.d-flex}\n::: {.bg-dark-blue-4 .color-swatch .text-white style=\"width: 160px; height: 40px;\"}\n.bg-dark-blue-4\n:::\n::: {.bg-dark-blue-3 .color-swatch .text-white style=\"width: 160px; height: 40px;\"}\n.bg-dark-blue-3\n:::\n::: {.bg-dark-blue-2 .color-swatch .text-white style=\"width: 160px; height: 40px;\"}\n.bg-dark-blue-2\n:::\n::: {.bg-dark-blue-1 .color-swatch style=\"width: 160px; height: 40px;\"}\n.bg-dark-blue-1\n:::\n::::::\n\n:::::: {.d-flex .mt-3}\n::: {.bg-orange-4 .color-swatch .text-white style=\"width: 160px; height: 40px;\"}\n.bg-orange-4\n:::\n::: {.bg-orange-3 .color-swatch .text-white style=\"width: 160px; height: 40px;\"}\n.bg-orange-3\n:::\n::: {.bg-orange-2 .color-swatch style=\"width: 160px; height: 40px;\"}\n.bg-orange-2\n:::\n::: {.bg-orange-1 .color-swatch style=\"width: 160px; height: 40px;\"}\n.bg-orange-1\n:::\n::::::\n\n:::::: {.d-flex .mt-3}\n::: {.bg-blue-4 .color-swatch .text-white style=\"width: 160px; height: 40px;\"}\n.bg-blue-4\n:::\n::: {.bg-blue-3 .color-swatch .text-white style=\"width: 160px; height: 40px;\"}\n.bg-blue-3\n:::\n::: {.bg-blue-2 .color-swatch style=\"width: 160px; height: 40px;\"}\n.bg-blue-2\n:::\n::: {.bg-blue-1 .color-swatch style=\"width: 160px; height: 40px;\"}\n.bg-blue-1\n:::\n::::::\n\n:::::: {.d-flex .mt-3}\n::: {.bg-silver-4 .color-swatch .text-white style=\"width: 160px; height: 40px;\"}\n.bg-silver-4\n:::\n::: {.bg-silver-3 .color-swatch .text-white style=\"width: 160px; height: 40px;\"}\n.bg-silver-3\n:::\n::: {.bg-silver-2 .color-swatch style=\"width: 160px; height: 40px;\"}\n.bg-silver-2\n:::\n::: {.bg-silver-1 .color-swatch style=\"width: 160px; height: 40px;\"}\n.bg-silver-1\n:::\n::::::\n\n:::::: {.d-flex .mt-3}\n::: {.bg-teal-4 .color-swatch style=\"width: 160px; height: 40px;\"}\n.bg-teal-4\n:::\n::: {.bg-teal-3 .color-swatch style=\"width: 160px; height: 40px;\"}\n.bg-teal-3\n:::\n::: {.bg-teal-2 .color-swatch style=\"width: 160px; height: 40px;\"}\n.bg-teal-2\n:::\n::: {.bg-teal-1 .color-swatch style=\"width: 160px; height: 40px;\"}\n.bg-teal-1\n:::\n::::::\n\n:::::: {.d-flex .mt-3}\n::: {.bg-r-blue-2 .color-swatch .text-white style=\"width: 160px; height: 40px;\"}\n.bg-r-blue-2\n:::\n::: {.bg-r-blue-1 .color-swatch .text-white style=\"width: 160px; height: 40px;\"}\n.bg-r-blue-1\n:::\n::::::\n\n### Text colors\n\n[.text-dark-blue-4]{.text-dark-blue-4}, [.text-dark-blue-3]{.text-dark-blue-3}, [.text-dark-blue-2]{.text-dark-blue-2}, [.text-dark-blue-1]{.text-dark-blue-1}\n\n[.text-orange-4]{.text-orange-4}, [.text-orange-3]{.text-orange-3}, [.text-orange-2]{.text-orange-2}, [.text-orange-1]{.text-orange-1}\n\n[.text-blue-4]{.text-blue-4}, [.text-blue-3]{.text-blue-3}, [.text-blue-2]{.text-blue-2}, [.text-blue-1]{.text-blue-1}\n\n[.text-silver-4]{.text-silver-4}, [.text-silver-3]{.text-silver-3}, [.text-silver-2]{.text-silver-2}, [.text-silver-1]{.text-silver-1}\n\n[.text-teal-4]{.text-teal-4}, [.text-teal-3]{.text-teal-3}, [.text-teal-2]{.text-teal-2}, [.text-teal-1]{.text-teal-1}\n\n[.text-r-blue-2]{.text-r-blue-2}, [.text-r-blue-1]{.text-r-blue-1}\n\n### Semantic colors\n\n[Primary](#){.btn .btn-primary .me-1}\n[Secondary](#){.btn .btn-secondary .me-1}\n[Success](#){.btn .btn-success .me-1}\n[Danger](#){.btn .btn-danger .me-1}\n[Warning](#){.btn .btn-warning .me-1}\n[Info](#){.btn .btn-info .me-1}\n[Light](#){.btn .btn-light .me-1}\n[Dark](#){.btn .btn-dark .me-1}\n\nThese will also apply to [Quarto callouts](https://quarto.org/docs/authoring/callouts.html).\n\n::: callout-note\nNote that there are five types of callouts, including: `note`, `tip`, `warning`, \n`caution`, and `important`.\n:::\n\n::: callout-warning\nCallouts provide a simple way to attract attention, for example, to this warning.\n:::\n\n::: callout-important\nThe callout heading is provided by the callout type, with the expected heading \n(i.e., Note, Warning, Important, Tip, or Caution).\n:::\n\n::: callout-tip\n## Tip With Title\n\nThis is an example of a callout with a title. Providing a callout heading is \noptional.\n:::\n\n::: {.callout-caution collapse=\"true\"}\n## Caution: Expand To Learn About Collapse\n\nThis is an example of a 'collapsed' caution callout that can be expanded by the \nuser. You can use `collapse=\"true\"` to collapse it by default or `collapse=\"false\"` \nto make a collapsible callout that is expanded by default.\n:::\n\nAnd they have utility classes for text colors:\n\n[.text-primary]{.text-primary}, [.text-primary-emphasis]{.text-primary-emphasis}, \n[.text-secondary]{.text-secondary}, [.text-secondary-emphasis]{.text-secondary-emphasis}, \n[.text-success]{.text-success}, [.text-success-emphasis]{.text-success-emphasis}, \n[.text-danger]{.text-danger}, [.text-danger-emphasis]{.text-danger-emphasis}, \n[.text-warning]{.text-warning}, [.text-warning-emphasis]{.text-warning-emphasis}, \n[.text-info]{.text-info}, [.text-info-emphasis]{.text-info-emphasis}, \n[.text-light]{.text-light}, [.text-light-emphasis]{.text-light-emphasis}, \n[.text-dark]{.text-dark}, [.text-dark-emphasis]{.text-dark-emphasis}, \n[.text-body]{.text-body}, [.text-body-emphasis]{.text-body-emphasis}, \n[.text-body-secondary]{.text-body-secondary}, [.text-body-tertiary]{.text-body-tertiary}\n\n## Fonts\n\nOur fonts are a mix of Rust Foundation fonts (Noto Sans and Serif) and IBM's \nPlex Mono. The monofont is typically used for headings and navbar, though Noto \nSerif may also be used. For body text, we use Noto Sans. Utility classes are \nprovided for each, though they should rarely be needed.\n\n:::::: {.d-flex .gap-2 .small}\n::: {.bg-dark-blue-4 .text-auto-dark .font-mono .p-3 .flex-fill}\n[IBM Plex Mono]{.fs-3} \n.font-mono \nABCDEFGHIJKLMNOPQRSTUVWXYZ \nabcdefghijklmnopqrstuvwxyz \n0123456789\n:::\n\n::: {.bg-dark-blue-3 .text-white .font-serif .p-3 .flex-fill}\n[Noto Serif]{.fs-3} \n.font-serif \nABCDEFGHIJKLMNOPQRSTUVWXYZ \nabcdefghijklmnopqrstuvwxyz \n0123456789\n:::\n\n::: {.bg-dark-blue-2 .text-auto-light .font-sans .p-3 .flex-fill}\n[Noto Sans]{.fs-3} \n.font-sans \nABCDEFGHIJKLMNOPQRSTUVWXYZ \nabcdefghijklmnopqrstuvwxyz \n0123456789\n:::\n::::::\n\n## Light and Dark\n\nThe `.bg-*` and `.text-*` utility classes automatically adapt to light and dark\nmode by flipping the tint scale — so `.text-dark-blue-4` in light mode becomes \n`.text-dark-blue-1` in dark mode, and `.text-dark-blue-1` in light mode becomes \n`.text-dark-blue-4` in dark mode. This means you generally don't need to\nthink about dark mode when using these classes. If you need a color to stay \nfixed regardless of the theme, however, simply append `-fixed` to the class \nname: `.bg-orange-4-fixed` and `.text-orange-4-fixed` will always render the \noriginal color. \n\nHere are some examples. To see how they work, toggle light and dark mode in the \nbrowser:\n\n[.bg-orange-4]{.bg-orange-4} \n[.bg-orange-4-fixed]{.bg-orange-4-fixed}\n\nNotice that the font changes from black to white in each example. This is \nBootstrap's color system adapting to the page background, which is meant to \nensure a certain contrast level for accessibility. Semantic colors (`.text-primary`, \n`.btn-success`, callouts, etc.) are also handled by Bootstrap's color system and \ndo not need any special treatment. However, you will notice that Bootstrap's \ncolor system does not recognize these utilities - currently, it does not even \nrecognize its own utilities! - so contrast can be lost when using them. To \nensure that accessibility is maintained, you may want to add `.text-auto-dark` \nor `.text-auto-light` classes, too. These will ensure that white and black text \nare correctly used with our utilities.\n\n[.bg-orange-1 with .text-auto-light]{.bg-orange-1 .text-auto-light} \n[.bg-orange-4 with .text-auto-dark]{.bg-orange-4 .text-auto-dark} \n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/contributing/documenting-code/execute-results/html.json b/_freeze/contributing/documenting-code/execute-results/html.json new file mode 100644 index 0000000..c18fc2d --- /dev/null +++ b/_freeze/contributing/documenting-code/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "2ab630bb6ca63d4ea341fb21efceb6c5", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Documenting Code\"\nengine: knitr\n---\n\nCode in extendr and rextendr should be thoroughly documented using widely\naccepted styling conventions in the R and Rust communities. \n\n## Writing style\n\n- Be concise! And stay on topic! The reader should not have to wade through\n lengthy digressions to get to the point.\n- Eschew obfuscation and avoid needlessly wordy phrases like 'eschew \n obfuscation.' In other words, keep it simple.\n- Second person (\"you\") is fine for instructions, but do not write about the \n reader's intentions in the second person (avoid expressions like \"you want to\" \n or \"you need to\").\n\n\n## Rust Documentation\n\n## R Documentation\n\nFunctions in rextendr should be documented with [roxygen2](https://roxygen2.r-lib.org), \nfollowing the conventions recommended by the [tidyverse style guide](https://style.tidyverse.org/documentation.html).\n\n- Every exported function in rextendr must include the following roxygen \n variables:\n - `@title`\n - `@description` — a concise description of what the function does.\n - `@param` — one tag per parameter, specifying:\n - the data type (e.g. `integer`, `character`, `logical`)\n - whether it is a scalar or vector\n - the default value, if one exists\n - `@return` — what the function returns, including the R type.\n- The `@details` and `@examples` sections are optional, but should in general be \n included.\n- Internal functions should include roxygen comments if their use in rextendr is \n fairly complex, important, and not easy to discern. Simple helper functions do\n not need to be documented.\n \n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/contributing/testing-code/execute-results/html.json b/_freeze/contributing/testing-code/execute-results/html.json new file mode 100644 index 0000000..08277e2 --- /dev/null +++ b/_freeze/contributing/testing-code/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "80bd5db10116f30dc51701d80d51de8f", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Testing Code\"\nengine: knitr\n---", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/contributing/user-guide-pages/execute-results/html.json b/_freeze/contributing/user-guide-pages/execute-results/html.json new file mode 100644 index 0000000..604e5e3 --- /dev/null +++ b/_freeze/contributing/user-guide-pages/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "3fa9b6d259a1b8f3143b11fe044bd7f7", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"User Guide Pages\"\nengine: knitr\n---\n\nUser guide pages are friendly introductions to specific features of the\n`extendr-api` crate. Keep them **concise** and use **simple language**.\nAlways use code chunks for extendr code — never paste raw snippets.\n\n### Page structure\n\nEach page should follow this rough structure:\n\n````markdown\n---\ntitle: \"An informative title\"\n---\n\n- Motivate the functionality the page covers\n- No more than 4 sentences\n\n## The feature\n\n- Describe the functionality\n- Discuss prerequisites if any\n\n## Basic usage\n\n- Discuss the code before showing it\n\n\n::: {.cell}\n\n```{.extendrsrc .cell-code}\n// your Rust code\n```\n:::\n\n\n- Summarize and explain the output\n\n## Advanced usage\n\n- Motivate the advanced example\n\n\n::: {.cell}\n\n```{.extendrsrc .cell-code}\n// your Rust code\n```\n:::\n\n\n- Summarize and explain\n\n## See also\n\n- Links to related pages or external docs\n````", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/contributing/writing-code/execute-results/html.json b/_freeze/contributing/writing-code/execute-results/html.json new file mode 100644 index 0000000..ce85827 --- /dev/null +++ b/_freeze/contributing/writing-code/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "eabdbdacaa25d5ec738f188b2ca6aa79", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"General writing conventions\"\nengine: knitr\n---\n\nThis page covers code formatting conventions for the extendr project. Following \nthese conventions keeps code consistent and readable across contributors.\n\n## R code\n\n- Never use `print()` to display an R object. Let R print it implicitly.\n- Use the `extendrsrc` and `extendr` knitr engines for all extendr code. See \n `user-guide/serde-integration.qmd` for a worked example to emulate.\n\n## Rust code\n\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/collections/execute-results/html.json b/_freeze/intro-rust/collections/execute-results/html.json new file mode 100644 index 0000000..1d1d45f --- /dev/null +++ b/_freeze/intro-rust/collections/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "579a7a4862437c7719c3c65c7e53088b", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Collections\"\nengine: knitr\n---\n\nRust has two primary ways to store multiple values of the same type: arrays and\nvectors. Arrays are fixed in size — their length is known at compile time, which \nmakes them fast but inflexible. You cannot add or remove elements after \ncreation. Vectors are like growable arrays, making them more flexible and also \nmore common in everyday Rust code. The syntax for creating arrays and vectors \nalso differs: \n\n```rust\n// array\nlet a = [10, 20, 30, 40];\n\n// vector\nlet v = vec![10, 20, 30, 40];\n```\n\nHere, the array is created using square brackets, the vector by calling the \n`vec!` macro. In this case, the types are inferred. If we want to make those\nexplicit, the syntax also differs:\n\n```rust\n// array\nlet a: [i32; 4] = [10, 20, 30, 40];\n\n// vector\nlet v: Vec = vec![10, 20, 30, 40];\n```\n\nIn this example, the array type and length are both declared with \n`[type; length]`, but the vector only requires specifying that it is a vector of \n32-bit integers or `Vec`, which can have any given length.\n\nOne nice thing about both vectors and arrays is that you can initialize them to \na specific constant value and size. The syntax here is similar, though again the\nvector version requires the use of the `vec!` macro:\n\n```rust\n// array\nlet a: [i32; 4] = [0; 4];\n\n// vector\nlet v: Vec = vec![0; 4];\n```\n\nIn each case, we put the initial value `0` followed by the size `4`.\n\nOne last thing to note here, specifically about vectors. An empty vector can \nalso be created with `Vec::new()` or `vec![]`. When creating an empty vector, \nhowever, Rust needs to know the element type either from context or from an \nexplicit type annotation, since it cannot infer type from the empty vector \nitself:\n\n```rust\nlet v: Vec = Vec::new();\n```\n\nVectors also have a number of useful **methods**: `.len()` returns the number of \nelements and `.is_empty()` returns `true` if there are no elements. We will \nencounter more methods like these as we work through the guide.\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/control-flow/execute-results/html.json b/_freeze/intro-rust/control-flow/execute-results/html.json new file mode 100644 index 0000000..dd086e3 --- /dev/null +++ b/_freeze/intro-rust/control-flow/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "b29bb5b357658aa0b68f590ff85d2acf", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Control Flow\"\nengine: knitr\n---\n\nControl flow refers to the ability to conditionally execute code. As with R, \nRust declares conditions for code execution using `if`, `else` and `else if`. \nThose conditions will typically involve logical operators:\n\n- `==` check equality\n- `!=` check inequality\n- `!` negate a logical value\n- `&&` logical AND comparison\n- `||` logical OR comparison\n\nLogical operators work similarly to R but are not vectorised. They return a \nsingle logical called a `bool` instead and can only take on one of two values: \n`true` or `false`. \n\nLike R, each branch of an `if` in Rust must be delimited by curly brackets, but \nunlike R, parentheses around the condition are not required:\n\n```rust\nif x == y {\n // do something\n} else {\n // do something else\n}\n```\n\nNote, too, that the result of control flow can be assigned to a variable:\n\n```rust\nlet number = if x == y { 5 } else { 3 };\n```\n\nGiven what we learned about Rust's strong typing system, this suggests an\nadditional strong constraint on control flow: each branch or \"arm\" of an `if` \nexpression must return the same type. Otherwise, the type would be unknown at \ncompile time. Consider this example program (drawn straight from The Book) that \nincludes mismatched types in the `if` and `else` arm:. \n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let condition = true;\n\n let number = if condition { 5 } else { \"six\" };\n\n println!(\"The value of number is: {number}\");\n}\n```\n\nIf you try to compile and run this program, you will get the following error\nmessage from cargo:\n\n```default\nerror[E0308]: `if` and `else` have incompatible types\n --> src/main.rs:4:44\n |\n4 | let number = if condition { 5 } else { \"six\" };\n | - ^^^^^ expected integer, found `&str`\n | |\n | expected because of this\n\nFor more information about this error, try `rustc --explain E0308`.\n```\n\nAs an side, this is actually a great example of one of Rust's best features. Not \nonly does the error message tell you where precisely it occurs, but it also \npoints you to additional information to help you understand what is wrong in \nyour code. Often, it will even suggest how to fix your problem!\n\nFor completeness, here is the correct way to program that control flow:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let condition = true;\n\n let number = if condition { 5 } else { 6 };\n\n println!(\"The value of number is: {number}\");\n}\n```\n\n```default\nThe value of number is: 5\n```", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/enumerations/execute-results/html.json b/_freeze/intro-rust/enumerations/execute-results/html.json new file mode 100644 index 0000000..872f92a --- /dev/null +++ b/_freeze/intro-rust/enumerations/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "797ed98c9824178cf9f540efbf61e5e6", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Enumerations\"\nengine: knitr\n---\n\nSometimes a value can only take on one of a fixed set of possibilities. In R,\nthis pattern appears constantly as function arguments. For example, the `cor()`\nfunction has the argument `method = c(\"pearson\", \"kendall\", \"spearman\")`. Rust \nformalizes this idea with *enumerations* or`enum`s, types that can take on \nexactly one of a defined set of *variants*.\n\n::: {.callout-note}\nThe tidyverse design style guide has a great section on enums. See \n[enumerate options](https://design.tidyverse.org/enumerate-options.html#whats-the-pattern).\n\nJosiah Parry's blog also has a nice discussion of this. See \n[Enums in R: towards type safe R](https://josiahparry.com/posts/2023-11-10-enums-in-r/).\n:::\n\nLike structs, enum names use PascalCase, and variants are created using \n`EnumName::Variant`:\n\n```rust\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\nlet my_shape = Shape::Triangle;\n```\n\n## Matching\n\nHow do we actually determine behavior based on a variant? This is done using \n*pattern matching*, specifically the keyword `match`, which works similar to \n`switch()` in R. The pattern match format uses the syntax `Enum::Variant => action`. \nWhen using `match` each variant much be *enumerated*: \n\n```{.rust filename=\"src/main.rs\"}\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\nfn main() {\n let my_shape = Shape::Triangle;\n\n match my_shape {\n Shape::Triangle => println!(\"A triangle has 3 vertices\"),\n Shape::Rectangle => println!(\"A rectangle has 4 vertices\"),\n Shape::Pentagon => println!(\"A pentagon has 5 vertices\"),\n Shape::Hexagon => println!(\"A hexagon has 6 vertices\"),\n }\n}\n```\n\n```default\nA triangle has 3 vertices\n```\n\nWhen you only care about specific variants, use `_` as a catch-all for\neverything else:\n\n```{.rust filename=\"src/main.rs\"}\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\nfn main() {\n let my_shape = Shape::Triangle;\n\n match my_shape {\n Shape::Hexagon => println!(\"Hexagons are the bestagons\"),\n _ => println!(\"Every other polygon is mid\"),\n }\n}\n```\n\n```default\nEvery other polygon is mid\n```\n\n## Methods\n\nEnums can have `impl` blocks just like structs. Inside the method, `match self` \nbranches on the variant:\n\n```{.rust filename=\"src/main.rs\"}\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\nimpl Shape {\n fn n_vertices(&self) -> i32 {\n match self {\n Self::Triangle => 3,\n Self::Rectangle => 4,\n Self::Pentagon => 5,\n Self::Hexagon => 6,\n }\n }\n}\n\nfn main() {\n let my_shape = Shape::Triangle;\n let n_vertices = my_shape.n_vertices();\n\n println!(\"my shape has {} vertices\", n_vertices);\n}\n```\n\n```default\nmy shape has 3 vertices\n```\n\n## Missing values\n\nBecause of its memory model and strong safety guarantees, Rust has no concept of \na `NULL` type. Null values are actually quite dangerous and have their own \ncategory of errors and security issues. In fact, [C.A.R. Hoare](https://en.wikipedia.org/wiki/Tony_Hoare), \nwho invented null pointers, called them his \"billion-dollar mistake.\" Rust,\ntherefore, makes it impossible to use null values, requiring instead that the \nconcept of missingness be implemented using a special kind of `enum` known as an\n`Option<>`, which has exactly two variants:\n\n```rust\nenum Option {\n Some(T),\n None,\n}\n```\n\nAs a reminder, the `T` here stands for any generic type. When a value is \npresent, it is wrapped in `Some(T)`. When it is absent, the variant is `None`. \nThe type system forces you to acknowledge the possibility of `None` before you \ncan use the inner value — there is no way to accidentally treat a missing value \nas if it were present.\n\nSince `Option` is just an enum, we can match on the variants to access its \nvalues.\n\n```rust\n// create an Option that contains a value\nlet measure = Some(Measure::Euclidean);\n\nmatch measure {\n Some(v) => println!(\"The measure is: {}\", v),\n None => println!(\"Oh no! The measure is missing!\"),\n}\n```\n\nThis makes missing values visible - the code cannot compile unless both arms are \nhandled.\n\n::: {.callout-warning}\nSometimes dealing with options is a headache, particularly when we're in the \nearly stages of developing. While we can use `.unwrap()` or `.expect()` to grab \nthe inner value of an option _without matching_, this is **dangerous!!** because \nwe are ignoring the possibility of a `None`. Unwrapping on a `None` leads to a \n**panic**. Panics cause your program to abort.\n\n```rust\nthread 'main' panicked at src/main.rs:4:41:\ncalled `Option::unwrap()` on a `None` value\n```\n:::\n\n## Optional arguments\n\nA common use is to make function arguments optional — call with a value when you \nhave one, and fall back to a default when you do not, similar to how we use \n`function(x = NULL)` in R. This can be achieved using an option in Rust.\n\n```{.rust filename=\"src/main.rs\"}\nfn greet_user(name: Option) {\n let user_name = name.unwrap_or(String::from(\"world\"));\n println!(\"Hello, {}!\", user_name);\n}\n\nfn main() {\n greet_user(None); // Output: Hello, world!\n greet_user(Some(\"Ferris\".to_string()));\n}\n```\n\n```default\nHello, world!\nHello, Ferris!\n```\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/functions/execute-results/html.json b/_freeze/intro-rust/functions/execute-results/html.json new file mode 100644 index 0000000..c685b81 --- /dev/null +++ b/_freeze/intro-rust/functions/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "6bf9354bc9ad4eeb32f73992c6b09998", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Functions\"\nengine: knitr\n---\n\nSo far, we have been modifying the `main()` function, but Rust lets us define as \nmany functions as we need. In R, functions are objects created with \n`<- function()`. In Rust, they are declared with the `fn` keyword. Just like \neverything else in Rust, arguments are typed - each argument takes the form \n`arg_name: type`, and the return type is specified after `->`.\n\n```rust\nfn name_of_function(arg1: ArgType) -> ReturnType {\n // function body\n my_return_object\n}\n```\n\nAs in R, the last expression in a function body is returned automatically — no \nexplicit `return` keyword is needed unless you are returning early. Functions \ncan also be declared anywhere in the file, outside of `main()`.\n\nIdentifying whether a number is odd or even is a classic example. The\n[is-odd npm package](https://www.npmjs.com/package/is-odd) became famous for \nbeing a remarkably small and widely depended-upon piece of code:\n\n![](/images/is-odd.png){width=70% fig-align=\"center\"}\n\nHere is the Rust equivalent. We define `is_even()` first, taking an `i32` and \nreturning a `bool`, then use it inside `is_odd()`:\n\n```rust\nfn is_even(x: i32) -> bool {\n x % 2 == 0\n}\n\nfn is_odd(x: i32) -> bool {\n !is_even(x)\n}\n```\n\nSince `x % 2 == 0` is an expression that evaluates to a `bool`, it is returned \ndirectly without a `return` statement. Building new functions from existing ones \nlike this is idiomatic Rust.\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/hello-world/execute-results/html.json b/_freeze/intro-rust/hello-world/execute-results/html.json new file mode 100644 index 0000000..3cf228c --- /dev/null +++ b/_freeze/intro-rust/hello-world/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "b679dd97539fd1e8e4d56b08cd59bfc1", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Hello, World!\"\nengine: knitr\n---\n\nIn this tutorial, you will learn how to setup a new Rust crate and get a sense\nfor what it includes. You will also learn how to build and run your new crate \nwith `cargo`, a package manager and build system for Rust. You do not need to \ninstall `cargo` because it comes bundled with Rust.\n\nWhen you first setup a Rust crate, you will have to choose between one of two \ntypes: binary crates and library crates. Binary crates are standalone \napplications or executables that you can run, like command line tools. Library \ncrates are typically small bundles of code that are used in the development of \nother crates. In this tutorial, we will focus on building a binary crate. \n\nTo create a new binary crate, open a terminal and run the following:\n\n```default\n$ cargo new hello-world\n``` \n\nThis will create a new directory called `hello-world` that you can then navigate \ninto using `cd hello-world`. Once inside that directory, you will find all of\nthe following:\n\n``` \nhello-world/\n├── Cargo.toml\n├── Cargo.lock\n└── src/\n └── main.rs\n```\n\nThe `Cargo.toml` file is a document in which you specify metadata and \ndependencies for your crate similar to an R package's `DESCRIPTION`. The \n`Cargo.lock` file maintains copies of those dependencies, similar to \n`renv.lock`. A lot more can be said about those two files, which are absolutely\ncrucial to programming in Rust, but for now let's focus on the Rust file in the \nsource directory. If you open that file in Positron (or your preferred IDE), you\nwill see the following text:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n println!(\"Hello, world!\");\n}\n```\n\nIn just those three short lines, we see examples of many Rust coding patterns:\n\n- Functions are declared using the `fn` keyword,\n- The `main()` function defines what happens when you run your binary,\n- Blocks of code are delimited using curly brackets (just like R), and\n- Statements end with `;`.\n\nRight now, our app uses the `println!()` macro to print \"Hello, world!\" to the \nterminal. When a program writes to the console, it does so through file \nconnections called standard output (`stdout`) and standard error (`stderr`). In \nR, when we print a message with `print()` or `message()`, we print to stdout. \nSimilarly, when we make a warning or error using `stop()` or `warning()`, that \nis writing to `stderr`. The Rust `println!()` macro also has the nice feature of \nsupporting string interpolation like `sprintf()` and `glue()`:\n\n``` rust\n// indirect interpolation\nlet name = \"Josiah\";\nprintln!(\"Hello, {}!\", name);\n\n// direct interpolation\nlet name = \"Josiah\";\nprintln!(\"Hello, {name}!\");\n```\n\nIf we add one or the other of those lines to `main()` like so\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let name = \"Josiah\";\n println!(\"Hello, {name}!\");\n}\n```\n\nwe can then call the application and get it to execute in the terminal:\n\n```default\n$ cargo run\n Compiling hello_world v0.1.0 (/hello-world)\n Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs\n Running `target/debug/hello_world`\nHello, Josiah!\n```\n\nNotice that `cargo` provides detailed information about the build process \n(printed to stderr) before actually running the program (and printing to \nstdout). This is a useful reminder that Rust, unlike R, is a compiled language - \nRust programs have to be built before they can be run. For those coming from R, \nthat might sound rather mysterious, and in some respects it is, but we want to \nemphasize how easy it was to get a program up and running. It was just three \nlines of code in the terminal:\n\n```default\n$ cargo new hello-world\n$ cd hello-world\n$ cargo run\n```\n\nThat's it! Those three lines were sufficient to build and run our simple binary. \nNow we can move on to learning about basic data types in Rust.\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/implementations/execute-results/html.json b/_freeze/intro-rust/implementations/execute-results/html.json new file mode 100644 index 0000000..641f18e --- /dev/null +++ b/_freeze/intro-rust/implementations/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "ea8a511a54069ebf5106815389a6c1a0", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Implementations\"\nengine: knitr\n---\n\nOther methods can be implemented for structs using `impl` blocks. An `impl` \nblock is roughly analogous to defining S3 methods in R, except Rust's `impl` \nblocks associate behavior directly with the type definition, making them \nobject-oriented. The syntax is this:\n\n```rust\nimpl TypeName {\n fn method_name() {\n // body\n }\n}\n```\n\n## Associated functions\n\nAn *associated function* is called on the type itself rather than on an \ninstance. These are written as `TypeName::function()` and are typically used\nas constructors. The return type `Self` refers to the type being implemented:\n\n```{.rust filename=\"src/main.rs\"} \n#[derive(Debug)]\nstruct Person {\n name: String,\n age: i32\n}\n\nimpl Person {\n fn new(name: String, age: i32) -> Self {\n Person { name, age }\n }\n}\n\nfn main() {\n let me = Person::new(\"Josiah\".to_string(), 29);\n println!(\"{me:?}\");\n}\n```\n\n```default\nPerson { name: \"Josiah\", age: 29 }\n```\n\nYou have already seen associated functions in this form: `Vec::new()` and\n`String::new()` are both associated functions.\n\n## Self-reference\n\nTo define a method that operates on an instance, add `&self` as the first\nargument. This provides a read-only reference to the struct, so you can access \nits fields but not move or modify them:\n\n```rust\nimpl Person {\n fn greet(&self) {\n println!(\n \"Hello, my name is {} and I am {} years old.\",\n self.name, self.age\n );\n }\n}\n```\n\nA method can also accept another instance of the same type by using `&Self` as \nan argument type. Here `is_older_than()` compares two `Person` values:\n\n```rust\nimpl Person {\n fn is_older_than(&self, other_person: &Self) -> bool {\n self.age > other_person.age\n }\n}\n```\n\n## Mutable self\n\nTo modify a struct's fields from a method, use `&mut self`. Here `rename()` and\n`celebrate_birthday()` both mutate fields in place:\n\n```{.rust filename=\"src/main.rs\"} \n#[derive(Debug)]\nstruct Person {\n name: String,\n age: i32\n}\n\nimpl Person {\n fn new(name: String, age: i32) -> Self {\n Person { name, age }\n }\n\n fn greet(&self) {\n println!(\n \"Hello, my name is {} and I am {} years old.\",\n self.name, self.age\n );\n }\n\n fn rename(&mut self, new_name: &str) {\n self.name = new_name.to_string();\n }\n\n fn celebrate_birthday(&mut self) {\n self.age += 1;\n }\n}\n\nfn main() {\n let mut me = Person::new(\"Josiah\".to_string(), 29);\n me.celebrate_birthday();\n me.rename(\"Jo\");\n me.greet(); \n}\n```\n\n```default\nHello, my name is Jo and I am 30 years old.\n```\n\nNote that the instance must also be declared `mut` when it is assigned to a \nvariable. ", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/iterators/execute-results/html.json b/_freeze/intro-rust/iterators/execute-results/html.json new file mode 100644 index 0000000..d33f97d --- /dev/null +++ b/_freeze/intro-rust/iterators/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "4a88c3e33f91c5f183d27f5f50c8fd2f", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Iterators\"\nengine: knitr\n---\n\nSo far we have been using `for` loops to work through collections, but Rust has \na more powerful and flexible mechanism: iterators. Iterators produce a sequence \nof items that can be accessed one at a time and chained with methods like \n`.sum()`, `.min()`, `.max()`, and `.enumerate()`. They are closer in spirit to \n`apply()` or `purrr::map()` than to a traditional for-loop.\n\nAnd actually, we were technically using iterators all along, since Rust calls \n`.into_iter()` on the vector passed to a for-loop under the hood. Both of these\nare equivalent:\n\n```rust\nlet nums = vec![3, 6, 9];\n\nfor n in nums { ... }\nfor n in nums.into_iter() { ... }\n```\n\n## Borrowing\n\nOne important constraint on the use of `.into_iter()` is that it actually \n*consumes* the vector — ownership is moved into the iterator so that the vector \ncan no longer be used afterward:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let nums = vec![3, 6, 9];\n for n in nums.into_iter() { // ⬅️ nums is moved\n println!(\"Value: {n}\");\n }\n \n println!(\"nums: {:?}\", nums); // ❌ compiler error\n}\n```\n\nTo iterate without consuming the original collection, use `.iter()` instead. \nThis *borrows* the collection and produces references to each element, leaving \nthe original value intact:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let nums = vec![3, 6, 9];\n \n for n in nums.iter() {\n println!(\"Reference to value: {n}\");\n }\n \n println!(\"nums is still usable: {:?}\", nums);\n}\n```\n\n```default\nReference to value: 3\nReference to value: 6\nReference to value: 9\nnums is still usable: [3, 6, 9]\n```\n\nOnce you have an iterator, a number of useful methods are available. Here is an \nexample of the sum method:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let nums = vec![2, 4, 8];\n let total: i32 = nums.iter().sum();\n println!(\"Sum is: {total}\");\n}\n```\n\n```default\nSum is: 14\n```\n\n## Enumerating\n\nYou can also `.enumerate()` over iterators to get the index and value of the \ncurrent element. These are returned as a tuple `(i, xi)` where `i` is the index \nand `xi` is the value. The data type of the index `i` is `usize`, which can if \nneeded be cast to `i32` with `i as i32`. Note that Rust is zero-indexed!\n\n```{.rust filename=\"src/main.rs\"} \nfn is_even(x: i32) -> bool {\n x % 2 == 0\n}\n\nfn main() {\n let x = vec![1, 3, 5];\n\n for (i, xi) in x.iter().enumerate() {\n if is_even(i as i32) {\n println!(\"Index {i} is even with value {xi}\");\n }\n }\n}\n```\n\n```default\nIndex 0 is even with value 1\nIndex 2 is even with value 5\n```\n\n## Mapping\n\nMaybe the most useful method to apply to iterators is `.map()`, the Rust analog \nof `purrr::map()` and the `apply()` family of functions. Here is an example that \nsquares each element of a vector:\n\n```rust\n// providing explicit type annotation in one\n// value so the compiler knows its f64 and not f32\nlet x = vec![5.9_f64, 6.8, 4.5, 7.3, 6.2];\n\nx.iter().map(|xi| xi.powi(2))\n```\n\nUsing `.map()` returns another iterator, so we can chain operations over \niterators by using multiple `.map()` statements.\n\n```rust\n// providing explicit type annotation in one\n// value so the compiler knows its f64 and not f32\nlet x = vec![5.9_f64, 6.8, 4.5, 7.3, 6.2];\n\nx.iter().map(|xi| xi.powi(2)).map(|xi| xi - 1.0)\n```\n\n## Closure\n\nIn these examples we use a *closure* to modify each element of the iterator, \nwhich is Rust's version of an anonymous function. The syntax of a closure is \n`|arg| expression`, similar to R's recent adoption of `\\(arg) expression`. Like \nR's anonymous functions, they can also be multiple lines by wrapping the \nexpression in curly brackets:\n\n```rust\nx.iter().map(|xi| {\n let squared = xi.powi(2);\n squared.sqrt()\n})\n```\n\nWhen using `.enumerate().map()`, you get the Rust version of `purrr::imap()`.\n\n```rust\nx.iter()\n .enumerate()\n .map(|(i, xi)| {\n // use both i and xi here\n })\n```\n\nNotice that the iterator is now the tuple `(i, xi)` as before in the for-loop.\n\n## Collecting\n\nIn R, `purrr::map()` always returns a list, but in Rust, `.map()` always returns \nanother iterator, so getting back a concrete collection means you must `.collect()` \nthe values in the iterator. Rust cannot always infer the type you want to \ncollect, however, so you need to provide it explicitly:\n\n```rust\nlet x_squared: Vec = x.iter().map(|xi| xi.powi(2)).collect();\n```\n\nAlternatively, you can use the *turbofish* syntax `.collect::()` to \nspecify the type directly on `.collect()` rather than in the assignment:\n\n```rust\nlet x_squared = x.iter().map(|xi| xi.powi(2)).collect::>();\n```\n\nIn either case, you may use a general placeholders to encourage Rust to infer \nthe type, the syntax being `Vec<_>`:\n\n```rust\nlet x_squared: Vec<_> = x.iter().map(|xi| xi.powi(2)).collect();\nlet x_squared = x.iter().map(|xi| xi.powi(2)).collect::>();\n```\n\nThis lets Rust infer the element type while you specify the container type.", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/loops/execute-results/html.json b/_freeze/intro-rust/loops/execute-results/html.json new file mode 100644 index 0000000..05dc0c2 --- /dev/null +++ b/_freeze/intro-rust/loops/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "2932b2ee3500d0c488e7b7a66b89fc78", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Loops\"\nengine: knitr\n---\n\nIn Rust, the easiest way to iterate over items in a collection is with a `for` \nloop. The syntax should look familiar to R programmers. Each item in the \ncollection is bound to the name you provide before `in` at each iteration of the \nloop:\n\n```rust\nfor value in collection {\n // do something with value\n}\n```\n\nAs with control flow, the content of a for-loop must be delimited by curly \nbrackets, but the in-statement does not require parantheses. As an example, \nconsider a program for printing each number in a vector:\n\n```{.rust filename=\"src/main.rs\"}\nfn main() {\n let nums = vec![1, 2, 3];\n\n for n in nums {\n println!(\"n is: {n}\");\n }\n}\n```\n\nRunning `cargo run --quiet` (the `quiet` suppresses compiler logs) should print\nthe following to your terminal:\n\n```default\nn is: 1\nn is: 2\nn is: 3\n```\n\nIn the context of for-loops, Rust's scoping rules are important to be aware of.\nThey are, in fact, much stricter than R's. Like R, values declared outside a \nfor-loop are accessible inside it, but unlike R, values created inside the loop \ncannot be used outside it. In the following example, `greeting` is defined in \nthe outer scope and is accessible inside the loop:\n\n```{.rust filename=\"src/main.rs\"}\nfn main() {\n let greeting = \"Hi\";\n let names = vec![\"Alice\", \"Bob\"];\n\n for name in names {\n println!(\"{greeting}, {name}!\");\n }\n}\n```\n\n```default\nHi, Alice!\nHi, Bob!\n```\n\nIn contrast, `doubled` in the next example is defined inside the loop and cannot \nbe accessed after it ends:\n\n```{.rust filename=\"src/main.rs\"}\nfn main() {\n let numbers = vec![1, 2, 3];\n\n for n in numbers {\n let doubled = n * 2;\n println!(\"{n} doubled is {doubled}\");\n }\n\n // ❌ `doubled` doesn't exist here\n // println!(\"Last doubled: {}\", doubled);\n}\n```\n\nIn the next section we will see how to get around this scoping rule using a \nmutable variable declared in the outer scope to collect values produced inside \nthe loop.\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/mutability/execute-results/html.json b/_freeze/intro-rust/mutability/execute-results/html.json new file mode 100644 index 0000000..3af2dc7 --- /dev/null +++ b/_freeze/intro-rust/mutability/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "b7fe49ef59ec05a339e046e1abdb500a", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Mutability\"\nengine: knitr\n---\n\nIn R, most objects are effectively immutable — when you modify one, R copies it \nfirst. This is called copy-on-write semantics. Rust takes a stricter approach, \nmaking variables immutable by default, and the compiler enforces this at compile \ntime. To make a variable mutable, you must declare it with the `mut` keyword:\n\n```rust\nlet mut x = 5;\nx = 6; // works because x is mutable\n```\n\nWithout `mut`, attempting to reassign a variable is a compile error. This helps \ncatch bugs early by making data changes explicit. Note that when updating a \nmutable variable, you do not re-bind it with `let` — you simply assign the new \nvalue directly.\n\nMutability is especially useful for accumulating values across a loop. In the \nprevious section, we saw that variables defined inside a loop cannot be accessed \noutside it. The solution is to declare a mutable variable in the outer scope and \nupdate it inside the loop:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let numbers = vec![1, 2, 3];\n let mut doubled = 0;\n\n for n in numbers {\n doubled = n * 2;\n println!(\"{} doubled is {}\", n, doubled);\n }\n\n // `doubled` is accessible here because it was declared outside the loop\n println!(\"Last doubled: {}\", doubled);\n}\n```\n\n```default\n1 doubled is 2\n2 doubled is 4\n3 doubled is 6\nLast doubled: 6\n``` \n\nYou can also use `+=` to add and assign in one step — `x += 10` is equivalent to\n`x = x + 10`. This makes computing running totals concise:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let x = vec![1.0, 2.0, 3.0, 4.0];\n let n = x.len() as f64;\n let mut total = 0.0;\n\n for xi in x {\n total += xi;\n }\n\n println!(\"The mean is: {}\", total / n);\n}\n```\n\n```default\nThe mean is: 2.5\n```\n\nLike scalar variables, vectors must also be declared `mut` to modify them after \ncreation. You can create an empty mutable vector and let Rust infer the data \ntype based on how you go on to use it:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let mut names = Vec::new();\n names.push(\"Alice\");\n names.push(\"Bob\");\n println!(\"{:?}\", names);\n}\n```\n\n```default\n[\"Alice\", \"Bob\"]\n```\n\nHere, `Vec::new()` creates an empty vector, and `.push()` appends an element to \nthe end. Because the element appended is a character string, the Rust compiler \nis able to infer the type of the names vector at compile time. With this \nexample, we have also introduced another method, namely `.push()`, which is \nparticularly useful, but there are, in fact, many ways to modify mutable vectors \nin Rust. Let us mention just three here.\n\n**Clear** -- Instead of adding elements, you can remove all elements at once \nwith `.clear()`:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let mut nums = vec![1, 2, 3];\n nums.clear();\n println!(\"{:?}\", nums);\n}\n```\n\n```default\n[]\n```\n\n**Sort** -- Vectors can be sorted in place with `.sort()`. Not all types support \nordering, but numeric types and strings do:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let mut x = vec![11, 3, 7, 10, 1];\n println!(\"x before sorting: {x:?}\");\n x.sort();\n println!(\"x after sorting: {x:?}\");\n}\n```\n\n```default\nx before sorting: [11, 3, 7, 10, 1]\nx after sorting: [1, 3, 7, 10, 11]\n```\n\n**Extend** -- To combine two vectors, use `.extend()`, which appends the \ncontents of one vector onto another: \n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let mut a = vec![1, 2];\n let b = vec![3, 4];\n a.extend(b);\n println!(\"{:?}\", a);\n}\n```\n\n```default\n[1, 2, 3, 4]\n```\n\nNote that the vector being extended must be `mut` and that the second vector is \nmoved into the first — it can no longer be used after the call. We will \nelaborate on that last point more in the section on Rust's memory model, \nspecifically the concept of ownership.", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/ownership/execute-results/html.json b/_freeze/intro-rust/ownership/execute-results/html.json new file mode 100644 index 0000000..5f8d1bf --- /dev/null +++ b/_freeze/intro-rust/ownership/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "3037bf525cc3ad58f03f1c8f8930a9ed", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Ownership\"\nengine: knitr\n---\n\nRust is notorious for its **borrow checker**, the mechanism that enforces memory \nsafety at compile time. The core rule is that every value has a single owner, \nand once that ownership is transferred, the original variable can no longer be \nused. This is called a **move**. A move occurs whenever a variable is passed to \na function or assigned to another variable. After the move, the original binding \nis invalid. Consider this example from The Book:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let s1 = String::from(\"hello\");\n let s2 = s1;\n println!(\"{s1}, world!\");\n}\n```\n\nRunning this code will cause the following error:\n\n```default\nerror[E0382]: borrow of moved value: `s1`\n --> src/main.rs:4:16\n |\n2 | let s1 = String::from(\"hello\");\n | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait\n3 | let s2 = s1;\n | -- value moved here\n4 | println!(\"{s1}, world!\");\n | ^^ value borrowed here after move\n |\n = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)\nhelp: consider cloning the value if the performance cost is acceptable\n |\n3 | let s2 = s1.clone();\n | ++++++++\n\n\nFor more information about this error, try `rustc --explain E0382`.\n```\n\nThere is a lot of output here, but let us focus on the source of the error for \nnow. First, we see that `let s2 = s1;` moves the value in `s1` to `s2`, and then\nwe are told that the `println!` macro mistakenly seeks to borrow the value that \nhas been moved. Since that value is no longer *owned* by `s1`, it causes an \nerror. \n\n## Cloning\n\nIn the example above, we are also given helpful advice about how to fix the \nerror using `.clone()`, which copies the value rather than moving it. Cloning is \nprobably the simplest way to reuse the original value, as almost everything in \nRust can be cloned.\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let s1 = String::from(\"hello\");\n let s2 = s1.clone();\n println!(\"{s1}, world!\");\n}\n```\n\n```default\nhello, world!\n```\n\n## Functions \n\nThe above example also highlights that the `println` macro seeks to *borrow* the \nvalue in `s1`. In fact, any time you pass a variable to a function, its value is \nmoved, meaning the originally defined variable can no longer be used to access \nthe value outside of the function. For example, let's define a `mean()` \nfunction:\n\n```rust\nfn mean(x: Vec) -> f64 {\n let mut total = 0.0;\n let n = x.len();\n \n for xi in x {\n total += xi;\n }\n \n total / (n as f64)\n}\n```\n\nCalling this function twice on the same vector will fail because the first call \nmoves `x` into the function:\n\n```rust\nlet x = vec![0.0, 3.14, 10.1, 44.8];\n\nlet avg1 = mean(x); // ⬅️ x moved here!\nlet avg2 = mean(x); // ❌ compiler error\n```\n\nAs we just saw, one way of handling this is to clone the data before passing it\nto the function.\n\n```rust\nlet x = vec![0.0, 3.14, 10.1, 44.8];\n\nlet avg1 = mean(x.clone()); // ⬅️ x cloned here!\nlet avg2 = mean(x); // ✅ compiler happy\n```\n\nWhile cloning in this way is safe and correct, it has the unfortunate downside \nof allocating a new copy of the data each time — not an issue for small vectors\nlike `x`, but as they grow, finding a better approach will become more urgent.\n\n## Borrowing \n\nFortunately, there is a more efficient approach known as *borrowing*, which \ninvolves passing a reference to a value rather than moving it. In Rust, we \nsignal a reference by placing an ampersand `&` in front of a variable or type \nname, for example, `&x` or `&Vec`. These tell the compiler that we are \nborrowing a value rather than taking ownership. To use the `mean()`function with \na reference, notice that we have to update its signature to make the reference \ntype explicit:\n\n```rust\nfn mean(x: &Vec) -> f64 {\n let mut total = 0.0;\n let n = x.len();\n \n for xi in x {\n total += *xi;\n }\n \n total / (n as f64)\n}\n\nfn main() {\n let x = vec![1.0, 2.0, 3.0];\n let avg = mean(&x); // 👈 borrowing `x`\n println!(\"x is still usable: {:?}\", x);\n}\n```\n\n```default\nx is still usable: [1.0, 2.0, 3.0]\n```\n\nAn important restriction on referencing is that borrowed values cannot be moved \nor mutated — only read. You can borrow a value as many times as you like, you \njust cannot change it.\n\n## Slices\n\nRather than reference the entire array or vector, we can reference a slice, or a \ncontiguous sequence of elements within it. These are denoted in Rust using `&[T]` \nwhere `T` stands for a generic type. For example, `&[f64]` means a reference \nslice of 64-bit floating points. If we want to demonstrate the use of reference \nslices with our `mean()` function, we will as before have to update its \nsignature to make that type explicit:\n\n```{.rust filename=\"src/main.rs\"} \nfn mean(x: &[f64]) -> f64 {\n let mut total = 0.0;\n let n = x.len();\n \n for xi in x {\n total += *xi;\n }\n \n total / (n as f64)\n}\n\nfn main() {\n let x = [0.0, 20.0, 742.3]; // array\n let y = vec![1.0, 2.0, 3.0]; // vector\n\n println!(\"the mean of x is: {}\", mean(&x));\n println!(\"the mean of y is: {}\", mean(&y));\n}\n```\n\n```default\nthe mean of x is: 254.1\nthe mean of y is: 2\n```\n\nThere are two things to note about this example. The first is very subtle. We \nupdated the signature for mean to take `x: &[f64]`, a slice. However, we passed \n`&x` and `&y` to the function, which are references to arrays and vectors. What \nis going on here? The answer is an implicit conversion. Both arrays and vectors\nhave methods for converting their generic references to slices, and those are\ninvoked when passing them into a function with this signature.\n\nFollowing from that, the second thing to note is that a function written to \naccept `&Vec` works only on vectors. That is because there is no method\nfor converting array references to vector references. But as we just saw, both \ncan be converted to slices. Rewriting a function to accept `&[f64]`, thus, makes \nit more general at no cost. So, in practice, we should prefer slices over vector \nreferences whenever the function only needs to read the data — it is more \nflexible and equally efficient.\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/structures/execute-results/html.json b/_freeze/intro-rust/structures/execute-results/html.json new file mode 100644 index 0000000..9ddae7e --- /dev/null +++ b/_freeze/intro-rust/structures/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "88dbb7412fdc7652681b1523934a4bff", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Structures\"\nengine: knitr\n---\n\nRust lets you define custom types by grouping related values together using the \n`struct` keyword. This is similar in spirit to a named list in R, but with fixed \nfields and a declared type. Struct names use PascalCase, while field names use \nsnake_case:\n\n```rust\nstruct Person {\n name: String,\n age: i32,\n}\n```\n\nA struct is created using \"literal\" syntax, where we write the struct name \nfollowed by curly brackets containing each field and its value:\n\n```rust\nlet me = Person { name: \"Josiah\".to_string(), age: 29 };\n```\n\n## Derive\n\nBy default, a new struct has no built-in behavior. It does not even have a \ndefined behavior for printing. So, right now, if you try to print the `Person` \nstruct, you will get an error.\n\n```{.rust filename=\"src/main.rs\"} \nstruct Person {\n name: String,\n age: i32\n}\n\nfn main() {\n let me = Person { name: \"Josiah\".to_string(), age: 29 };\n println!(\"{me:?}\");\n}\n```\n\n```default\nerror[E0277]: `Person` doesn't implement `Debug`\n --> src/main.rs:8:15\n |\n8 | println!(\"{me:?}\");\n | ^^^^^^ `Person` cannot be formatted using `{:?}` because it doesn't implement `Debug`\n |\n = help: the trait `Debug` is not implemented for `Person`\n = note: add `#[derive(Debug)]` to `Person` or manually `impl Debug for Person`\n = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)\nhelp: consider annotating `Person` with `#[derive(Debug)]`\n |\n1 + #[derive(Debug)]\n2 | struct Person {\n |\n\nFor more information about this error, try `rustc --explain E0277`.\n```\n\nHere again we see a great demonstration of Rust's very informative error \nmessages. In this case, it also suggests a solution. We should derive the \n`Debug` trait. What is a trait exactly? In Rust, traits are similar to R's S3 \ngenerics — they define a set of methods that a type can support. In this case, \nthe `Debug` trait allows for printing the content of a struct to stdout using \nthe special `:?` syntax in `println!()` or the `dbg!()` macro. \n\nFor very simple behaviors like printing, it is possible to derive the behavior \nautomatically from the struct definition itself. In fact, this is what the error \nmessage above is telling us to do. It tells us to use another type of macro, \nspecifically the `derive` macro, placing it above the struct definition and \ntelling it what specific behavior to derive, in this case, `Debug`.\n\n```{.rust filename=\"src/main.rs\"} \n#[derive(Debug)]\nstruct Person {\n name: String,\n age: i32\n}\n\nfn main() {\n let me = Person { name: \"Josiah\".to_string(), age: 29 };\n println!(\"{me:?}\");\n}\n```\n\n```default\nPerson { name: \"Josiah\", age: 29 }\n```\n\n## Fields \n\nFields in a struct can be accessed with dot notation: `me.name`, `me.age`. But \nbeware that ownership rules apply here, too. Accessing a field by moving it out \nof the struct partially moves the struct itself, making the whole struct \nunusable afterward:\n\n```{.rust filename=\"src/main.rs\"} \n#[derive(Debug)]\nstruct Person {\n name: String,\n age: i32,\n}\n\nfn main() {\n let me = Person { name: \"Josiah\".to_string(), age: 29 };\n let name = me.name; // ⬅️ name moved out\n println!(\"{me:?}\"); // ❌ error: partial move\n}\n```\n\n```default\nerror[E0382]: borrow of partially moved value: `me`\n --> src/main.rs:10:16\n |\n 9 | let name = me.name; // name moved out\n | ------- value partially moved here\n10 | println!(\"{me:?}\"); // error: partial move\n | ^^ value borrowed here after partial move\n |\n = note: partial move occurs because `me.name` has type `String`, which does not implement the `Copy` trait\n = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)\n\nFor more information about this error, try `rustc --explain E0382`.\n```\n\nTo read a field without consuming it, borrow it instead by referencing the \nstruct.\n\n```{.rust filename=\"src/main.rs\"} \n#[derive(Debug)]\nstruct Person {\n name: String,\n age: i32,\n}\n\nfn main() {\n let me = Person { name: \"Josiah\".to_string(), age: 29 };\n let name = &me.name;\n println!(\"{me:?}\");\n}\n```\n\n```default\nPerson { name: \"Josiah\", age: 29 }\n```\n\n## Destructuring\n\nRust lets you unpack all fields of a struct in a single `let` binding. This is \ncalled destructuring and uses the syntax `let StructName { field1, field2 } = variable`:\n\n```{.rust filename=\"src/main.rs\"} \nfn main() {\n let me = Person { name: \"Josiah\".to_string(), age: 29 };\n let Person { name, age } = me;\n println!(\"{name} is {age} years old\");\n}\n```\n\n```default\nJosiah is 29 years old\n```\n\nDestructuring is useful when you need several fields at once and want to avoid\nrepeated `.field` access. The struct is consumed in the process, so the original \nvariable is no longer available.\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/intro-rust/types/execute-results/html.json b/_freeze/intro-rust/types/execute-results/html.json new file mode 100644 index 0000000..64ac31e --- /dev/null +++ b/_freeze/intro-rust/types/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "8025007c7e286f43872d1bda0692bb8c", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Basic Types\"\nengine: knitr\n---\n\nIn R, everything is a vector or a *collection* of values. There is no concept of\na single value variable or scalar. The closest equivalent to that is a \nlength-one vector. In Rust, however, scalars are the building blocks of \neverything. When a collection is needed, it is made directly from scalars. For \nthis reason, you will often hear them referred to as **primitives**. A \nprimitive or scalar can be used to hold a single value of a specific data type, \nincluding strings, integers, and floats, among others. In this tutorial, we will \ncover some of those data types, focusing on how to instantiate them and convert\nbetween them.\n\nIntegers in Rust are a good place to start. They can be either signed, meaning \nthey can represent negative values, or unsigned, meaning they support only \npositive values. Both varieties come in several sizes depending on how many bits \nare needed to store the value:\n\n- Signed integers: `i8`, `i16`, `i32`, `i64`, `i128`\n- Unsigned integers: `u8`, `u16`, `u32`, `u64`, `u128`\n\nThe letter prefix indicates the type (`i` for signed, `u` for unsigned) and the \nnumber indicates the bit width. Unsigned integers can hold larger positive \nvalues than their signed counterparts because they do not need to use a bit to \nrepresent a negative value:\n\n``` rust\ni32::MAX // 2147483647\nu32::MAX // 4294967295\n```\n\nFloating points are similar to integers but can represent decimal values, too.\nUnlike integers, floating points are also always signed and come in two sizes: \n`f32` and `f64`. \n\n::: {.callout-note}\nIn R, integer vectors are comprised of only `i32` values, and floating points, \ncalled doubles, are always `f64` values.\n:::\n\nRust can usually infer types. For instance, it can distinguish the integer `1` \nfrom the floating point `1.0`, but you can also specify the type explicitly. \nThere are two ways to do this — using `:` in the assignment, or by appending a \ntype suffix to the literal value:\n\n``` rust\nlet x: f64 = 10.0;\nlet x = 10.0f64;\n```\n\nYou can also use `_` as a visual separator when writing numeric literals. The\nfollowing are all identical:\n\n``` rust\nlet x: i32 = 1000;\nlet x = 1000i32;\nlet x = 1_000_i32;\n```\n\nThis pattern of explicitly declaring the type of a variable may seem somewhat \nverbose to the average R user since R is very lax about types, relying as it \ndoes on a lot of implicit conversions. Becuase of Rust's memory management \nmodel, however, Rust is a strongly, statically typed language. That means that \nall types must be known when you go to build or compile your crate. It also \nprecludes running operations on different types. For example, math expressions \ncan only be performed between values of the same type, so you cannot, for \nexample, directly add an `f64` and an `i32`. You must first cast one to match \nthe type of the other, in the simplest case using the `as` keyword:\n\n``` rust\nfn add2(x: f64, y: i32) -> f64 {\n x + (y as f64)\n}\n```\n\nIf you call `add2(3.2, 2)`, the function will first cast `y` to `f64` before\ndoing addition, returning the `f64` value `5.2`. Before we continue, note that \nRust supports all the same numeric operators as R: `+`, `-`, `/`, `*`, and `%` \nfor remainder (equivalent to `%%` in R).", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_freeze/user-guide/extendr-macro/execute-results/html.json b/_freeze/user-guide/extendr-macro/execute-results/html.json new file mode 100644 index 0000000..5c1ba96 --- /dev/null +++ b/_freeze/user-guide/extendr-macro/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "5a0471ae02813c293ee464427b6f0104", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"The `#[extendr]` macro\"\nexecute: \n eval: false\n---\n\nThe central task of extendr is to make Rust code available in R. Crucial to this\neffort is the attribute macro, `#[extendr]`. This chapter provides a brief \nintroduction to the use of that macro. \n\n::: {.callout-note}\nIf you would like to learn more about Rust macros, check The Book, specifically \n[Part 5 of Ch. 20](https://doc.rust-lang.org/book/ch20-05-macros.html). You may \nalso want to consult the section on [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html)\nin the official Rust reference manual.\n:::\n\n\n\nRust allows attribute macros to be placed on functions, struct, enums, and \nimpls, and extendr supports all of these. The general pattern is mostly the same\nin each case, but for starters, let's consider how to apply the extendr macro to\na Rust function in order to make it available in R. In order for that to happen, \nthree things need to happen. First, you need to declare the extendr namespace to \nthe compiler with `use`. After that, you need to annotate whatever function you \nwant to make available in R with the `#[extendr]` macro. Finally, you must \nregister the function in the `extendr_module! {}` macro. \n\nAs an example, suppose that you have created a function that gives the answer to \nthe question \"What is the meaning of life?\" and that you have very thoughtfully \ndecided to share this wisdom with the R community. Your main Rust library script \nshould then look like this:\n\n```{.rust filename=\"src/lib.rs\"}\nuse extendr_api::prelude::*;\n\n#[extendr]\nfn answer_to_life() -> i32 {\n 42\n}\n\nextendr_module! {\n mod hitchhiker;\n fn answer_to_life;\n}\n```\n\nHere, the `#[extendr]` macro is placed directly above the function definition.\n\n\n## `ToVectorValue` trait\n\nIn order for an item to be returned from a function marked with the `#[extendr]`\nattribute macro, it must be able to be turned into an R object. In extendr, the\nstruct `Robj` is a catch all for any type of R object.\n\n::: callout-note\nFor those familiar with PyO3, the `Robj` struct is similar in concept to the\n[`PyAny`](https://docs.rs/pyo3/latest/pyo3/types/struct.PyAny.html) struct.\n:::\n\nThe `ToVectorValue` trait is what is used to convert Rust items into R objects.\nThe trait is implemented on a number of standard Rust types such as `i32`,\n`f64`, `usize`, `String` and more (see [all foreign implementations\nhere](https://extendr.github.io/extendr/extendr_api/robj/into_robj/trait.ToVectorValue.html#foreign-impls))\nwhich enables these functions to be returned from a Rust function marked with\n`#[extendr]`.\n\n::: callout-note\nIn essence, all items that are returned from a function must be able to be\nturned into an `Robj`. Other extendr types such as `List`, for example, have a\n`From for Robj` implementation that defines how it is converted into an\n`Robj`.\n:::\n\nThis means that with a little extra work, the `Shape` enum can be returned to R.\nTo do so, the `#[extendr]` macro needs to be added to an impl block.\n\n## Exporting `impl` blocks\n\nThe other supported item that can be made available to R is an\n[`impl`](https://doc.rust-lang.org/std/keyword.impl.html) block. `impl` is a\nkeyword that allows you to *implement* a trait or an inherent implementation.\nThe `#[extendr]` macro works with inherent implementations. These are `impl`s on\na type such as an `enum` or a `struct`. extendr *does not* support using\n`#[extendr]` on trait impls.\n\n::: callout-note\nYou can only add an inherent implementation on a type that you have own and not\nprovided by a third party crate. This would violate the [orphan\nrules](https://github.com/Ixrec/rust-orphan-rules?tab=readme-ov-file#what-are-the-orphan-rules).\n:::\n\nContinuing with the `Shape` example, this enum alone cannot be returned to R.\nFor example, the following code will result in a compilation error\n\n```rust\n#[derive(Debug)]\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\n#[extendr]\nfn make_shape(shape: &str) -> Shape {\n match shape {\n \"triangle\" => Shape::Triangle,\n \"rectangle\" => Shape::Rectangle,\n \"pentagon\" => Shape::Pentagon,\n \"hexagon\" => Shape::Hexagon,\n &_ => unimplemented!()\n }\n}\n```\n\n``` \nerror[E0277]: the trait bound `Shape: ToVectorValue` is not satisfied\n --> src/lib.rs:19:1\n |\n19 | #[extendr]\n | ^^^^^^^^^^ the trait `ToVectorValue` is not implemented for `Shape`, which is required by `extendr_api::Robj: From`\n |\n```\n\nHowever, you add the `#[extendr]` attribute to the `Shape` enum, it can be\nreturned to R.\n\n\n::: {.cell}\n\n```{.extendrsrc .cell-code}\n#[derive(Debug)]\n#[extendr]\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\n#[extendr]\nfn make_shape(shape: &str) -> Shape {\n match shape {\n \"triangle\" => Shape::Triangle,\n \"rectangle\" => Shape::Rectangle,\n \"pentagon\" => Shape::Pentagon,\n \"hexagon\" => Shape::Hexagon,\n &_ => unimplemented!()\n }\n}\n\n```\n:::\n\n\nIt is also possible to add methods to structs/enums and their instances:\n\n\n::: {.cell}\n\n```{.extendrsrc .cell-code}\n#[derive(Debug)]\n#[extendr]\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\n#[extendr]\nimpl Shape {\n fn new(x: &str) -> Self {\n match x {\n \"triangle\" => Self::Triangle,\n \"rectangle\" => Self::Rectangle,\n \"pentagon\" => Self::Pentagon,\n \"hexagon\" => Self::Hexagon,\n &_ => unimplemented!(),\n }\n }\n\n fn n_coords(&self) -> usize {\n match &self {\n Shape::Triangle => 3,\n Shape::Rectangle => 4,\n Shape::Pentagon => 4,\n Shape::Hexagon => 5,\n }\n }\n}\n```\n:::\n\n\nIn this example two new methods are added to the `Shape` enum. The first `new()`\nis like the `make_shape()` function that was shown earlier: it takes a `&str`\nand returns an enum variant. Now that the enum has an `impl` block with\n`#[extendr]` attribute macro, it can be exported to R by inclusion in the\n`extendr_module! {}` macro.\n\n```rust\nextendr_module! {\n mod hellorust;\n impl Shape;\n}\n```\n\nDoing so creates an environment in your package called `Shape`. The environment\ncontains all of the methods that are available to you.\n\n::: callout-tip\nThere are use cases where you may not want to expose any methods but do want to\nmake it possible to return a struct or an enum to the R. You can do this by\nadding an empty impl block with the `#[extendr]` attribute macro.\n:::\n\nIf you run `as.list(Shape)` you will see that there are two functions in the\nenvironment which enable you to call the methods defined in the impl block. You\nmight think that this feel like an [R6\nobject](https://r6.r-lib.org/articles/Introduction.html) and you'd be right\nbecause an R6 object essentially is an environment!\n\n\n::: {.cell}\n\n```{.r .cell-code}\nas.list(Shape)\n```\n:::\n\n\nCalling the `new()` method instantiates a new enum variant.\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntri <- Shape$new(\"triangle\")\ntri\n```\n:::\n\n\nThe newly made `tri` object is an [external\npointer](https://cran.r-project.org/doc/manuals/R-exts.html#External-pointers-and-weak-references)\nto the `Shape` enum in Rust. This pointer has the same methods as the Shape\nenvironment—though they cannot be seen in the same way. For example you can run\nthe `n_coords()` method on the newly created object.\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntri$n_coords()\n```\n:::\n\n\n::: callout-tip\nTo make the methods visible to the `Shape` class you can define a `.DollarNames`\nmethod which will allow you to preview the methods and attributes when using the\n`$` syntax. This is very handy to define when making an impl a core part of your\npackage.\n\n\n::: {.cell}\n\n```{.r .cell-code}\n.DollarNames.Shape = function(env, pattern = \"\") {\n ls(Shape, pattern = pattern)\n}\n```\n:::\n\n:::\n\n### `impl` ownership\n\nAdding the `#[extendr]` macro to an impl allows the struct or enum to be made\navailable to R as an external pointer. Once you create an external pointer, that\nis then owned by R. So you can only get references to it or mutable references.\nIf you need an owned version of the type, then you will need to clone it.\n\n## Accessing exported `impl`s from Rust\n\nInvariably, if you have made an impl available to R via the `#[extendr]` macro,\nyou may want to define functions that take the impl as a function argument.\n\nDue to R owning the `impl`'s external pointer, these functions cannot take an\nowned version of the impl as an input. For example trying to define a function\nthat subtracts an integer from the `n_coords()` output like below returns a\ncompiler error.\n\n```rust\n#[extendr]\nfn subtract_coord(x: Shape, n: i32) -> i32 {\n (x.n_coords() as i32) - n\n}\n```\n\n``` \nthe trait bound `Shape: extendr_api::FromRobj<'_>` is not satisfied\n --> src/lib.rs:53:22\n |\n | fn subtract_coord(x: Shape, n: i32) -> i32 {\n | ^^^^^ the trait `extendr_api::FromRobj<'_>` is not implemented for `Shape`\n |\nhelp: consider borrowing here\n |\n | fn subtract_coord(x: &Shape, n: i32) -> i32 {\n | +\n | fn subtract_coord(x: &mut Shape, n: i32) -> i32 {\n | ++++\n```\n\nAs most often, the compiler's suggestion is a good one. Use `&Shape` to use a\nreference.\n\n## `ExternalPtr`: returning arbitrary Rust types\n\nIn the event that you need to return a Rust type to R that doesn't have a\ncompatible impl or is a type that you don't own, you can use `ExternalPtr`.\nThe `ExternalPtr` struct allows any item to be captured as a pointer and\nreturned to R.\n\nHere, for example, an `ExternalPtr` is returned from the `shape_ptr()`\nfunction.\n\n::: callout-tip\nAnything that is wrapped in `ExternalPtr` must implement the `Debug` trait.\n:::\n\n\n::: {.cell}\n\n```{.extendrsrc .cell-code}\n#[derive(Debug)]\nenum Shape {\n Triangle,\n Rectangle,\n Pentagon,\n Hexagon,\n}\n\n#[extendr]\nfn shape_ptr(shape: &str) -> ExternalPtr {\n let variant = match shape {\n \"triangle\" => Shape::Triangle,\n \"rectangle\" => Shape::Rectangle,\n \"pentagon\" => Shape::Pentagon,\n \"hexagon\" => Shape::Hexagon,\n &_ => unimplemented!(),\n };\n\n ExternalPtr::new(variant)\n}\n```\n:::\n\n\nUsing an external pointer, however, is far more limiting than the `impl` block.\nFor example, you cannot access any of its methods.\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntri_ptr <- shape_ptr(\"triangle\")\ntri_ptr$n_coords()\n```\n:::\n\n\nTo use an `ExternalPtr`, you have to go through a bit of extra work for it.\n\n\n\n```rust\n#[extendr]\nfn n_coords_ptr(x: Robj) -> i32 {\n let shape = TryInto::>::try_into(x); \n \n match shape {\n Ok(shp) => shp.n_coords() as i32,\n Err(_) => 0\n }\n}\n```\n\nThis function definition takes an `Robj` and from it, tries to create an\n`ExternalPtr`. Then, if the conversion did not error, it returns the\nnumber of coordinates as an `i32` (R's version of an integer) and if there was\nan error converting, it returns 0.\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntri_ptr <- shape_ptr(\"triangle\")\n\nn_coords_ptr(tri_ptr)\n\nn_coords_ptr(list())\n```\n:::\n\n\nFor a good example of using `ExternalPtr` within an R package, refer to the\n[`b64` R package](https://github.com/extendr/b64).", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/_quarto.yml b/_quarto.yml index d2edc48..03eb475 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -14,89 +14,100 @@ website: repo-url: https://github.com/extendr/extendr.github.io repo-branch: main repo-actions: [edit, issue] + favicon: "/images/extendr-gears-logo.png" + page-navigation: true + bread-crumbs: true navbar: - logo: /images/feRris.png - left: - - text: "Get Started" + search: true + right: + - text: "INSTALL" href: get-started.qmd - - text: Documentation + - text: DOCS menu: - - text: "Rust crates" - href: "https://extendr.github.io/extendr/" + - text: "{extendr} crates" + href: https://extendr.github.io/extendr/ - text: "{rextendr} package" href: https://extendr.github.io/rextendr/dev - - user-guide/index.qmd - - intro-rust/index.qmd - - text: "Blog" + - text: "Rust Basics" + href: intro-rust/index.qmd + - text: "User Guide" + href: user-guide/index.qmd + - text: "Contributing" + href: contributing/index.qmd + - text: "NEWS" href: blog/index.qmd - right: - icon: github href: https://github.com/extendr/extendr - icon: discord href: https://discord.gg/7hmApuc sidebar: - - title: "Rust for R Developers" - style: "floating" - collapse-level: 1 + - id: rust-basics + title: "Rust Basics" + collapse-level: 2 contents: - - intro-rust/index.qmd - - intro-rust/why-rust.qmd - - intro-rust/hello-world.qmd - - intro-rust/types.qmd - - intro-rust/fizz-buzz.qmd - - intro-rust/collections.qmd - - intro-rust/for-loops.qmd - - intro-rust/mutability.qmd - - intro-rust/mutable-vectors.qmd - - intro-rust/is-odd.qmd - - intro-rust/references-slices.qmd - - intro-rust/iterators.qmd - - intro-rust/iter-map.qmd - - intro-rust/structs.qmd - - intro-rust/struct-methods.qmd - - intro-rust/enums.qmd - - intro-rust/options.qmd + - section: "Rust Basics" + href: intro-rust/index.qmd + contents: + - intro-rust/hello-world.qmd + - intro-rust/types.qmd + - intro-rust/control-flow.qmd + - intro-rust/collections.qmd + - intro-rust/loops.qmd + - intro-rust/mutability.qmd + - intro-rust/functions.qmd + - intro-rust/ownership.qmd + - intro-rust/iterators.qmd + - intro-rust/structures.qmd + - intro-rust/implementations.qmd + - intro-rust/enumerations.qmd - - title: "User Guide" - collapse-level: 1 - style: "floating" + - id: guide + title: "User Guide" + collapse-level: 3 contents: - - user-guide/index.qmd - - user-guide/complete-example.qmd - - section: "R Packages" + - section: "User Guide" + href: user-guide/index.qmd contents: - - user-guide/r-pkgs/package-setup.qmd - - user-guide/r-pkgs/package-structure.qmd - - section: "Type Mapping" - contents: - - user-guide/type-mapping/extendr-macro.qmd - - user-guide/type-mapping/scalars.qmd - - user-guide/type-mapping/vectors.qmd - - user-guide/type-mapping/collections.qmd - - user-guide/type-mapping/into-list.qmd - - user-guide/type-mapping/missing-values.qmd - - user-guide/type-mapping/characters.qmd - - section: "Error Handling" + - user-guide/complete-example.qmd + - section: "R Packages" + contents: + - user-guide/package-structure.qmd + - section: "Publishing" + + - id: contributing + title: "Contributing" + collapse-level: 2 + contents: + - section: "Contributing" + href: contributing/index.qmd contents: - - user-guide/error-handling/basic-error-handling.qmd - - user-guide/default-args.qmd - - user-guide/serde-integration.qmd - - user-guide/tokio.qmd - - user-guide/cran-publishing.qmd - - text: CRAN's MSRV - href: user-guide/cran-msrv.qmd - - text: WebR - href: user-guide/webr.qmd - - text: FAQ - href: user-guide/faq.qmd + - contributing/writing-code.qmd + - contributing/documenting-code.qmd + - contributing/testing-code.qmd + - contributing/extendr-internals.qmd + - contributing/rextendr-internals.qmd + - contributing/colors-and-fonts.qmd format: html: html-table-processing: none - theme: css/_bootswatch.scss + theme: + light: [flatly, css/extendr.scss] + dark: [darkly, css/extendr.scss] toc: true - include-in-header: - - text: | - + toc-location: right + grid: + sidebar-width: 250px + body-width: 900px + margin-width: 300px + +# this currently does not work +# see: https://github.com/quarto-dev/quarto-cli/issues/3157 +engine: knitr + +knitr: + opts_chunk: + collapse: true + comment: "#>" \ No newline at end of file diff --git a/_variables.yml b/_variables.yml new file mode 100644 index 0000000..4154425 --- /dev/null +++ b/_variables.yml @@ -0,0 +1,5 @@ +version: + r: "4.2" + rust: "1.65.0" + extendr: "0.8.1" + rextendr: "0.4.2.9000" diff --git a/contributing/colors-and-fonts.qmd b/contributing/colors-and-fonts.qmd new file mode 100644 index 0000000..d2955e5 --- /dev/null +++ b/contributing/colors-and-fonts.qmd @@ -0,0 +1,246 @@ +--- +title: "Colors and Fonts" +engine: knitr +--- + +```{=html} + +``` + +Our colors and themes are inspired by the delightfully awful 90's color choices +of the R Foundation, particularly [its logo](https://www.r-project.org/logo/), +and the Rust Foundation's official [brand guide](https://rustfoundation.org/brand-guide/). + +## Colors + +We have defined several utility classes to make using consistent colors easier. +These can be used to define background and text colors. They are also used to +define semantic colors, which are applied automatically. + +### Background colors + +:::::: {.d-flex} +::: {.bg-dark-blue-4 .color-swatch .text-white style="width: 160px; height: 40px;"} +.bg-dark-blue-4 +::: +::: {.bg-dark-blue-3 .color-swatch .text-white style="width: 160px; height: 40px;"} +.bg-dark-blue-3 +::: +::: {.bg-dark-blue-2 .color-swatch .text-white style="width: 160px; height: 40px;"} +.bg-dark-blue-2 +::: +::: {.bg-dark-blue-1 .color-swatch style="width: 160px; height: 40px;"} +.bg-dark-blue-1 +::: +:::::: + +:::::: {.d-flex .mt-3} +::: {.bg-orange-4 .color-swatch .text-white style="width: 160px; height: 40px;"} +.bg-orange-4 +::: +::: {.bg-orange-3 .color-swatch .text-white style="width: 160px; height: 40px;"} +.bg-orange-3 +::: +::: {.bg-orange-2 .color-swatch style="width: 160px; height: 40px;"} +.bg-orange-2 +::: +::: {.bg-orange-1 .color-swatch style="width: 160px; height: 40px;"} +.bg-orange-1 +::: +:::::: + +:::::: {.d-flex .mt-3} +::: {.bg-blue-4 .color-swatch .text-white style="width: 160px; height: 40px;"} +.bg-blue-4 +::: +::: {.bg-blue-3 .color-swatch .text-white style="width: 160px; height: 40px;"} +.bg-blue-3 +::: +::: {.bg-blue-2 .color-swatch style="width: 160px; height: 40px;"} +.bg-blue-2 +::: +::: {.bg-blue-1 .color-swatch style="width: 160px; height: 40px;"} +.bg-blue-1 +::: +:::::: + +:::::: {.d-flex .mt-3} +::: {.bg-silver-4 .color-swatch .text-white style="width: 160px; height: 40px;"} +.bg-silver-4 +::: +::: {.bg-silver-3 .color-swatch .text-white style="width: 160px; height: 40px;"} +.bg-silver-3 +::: +::: {.bg-silver-2 .color-swatch style="width: 160px; height: 40px;"} +.bg-silver-2 +::: +::: {.bg-silver-1 .color-swatch style="width: 160px; height: 40px;"} +.bg-silver-1 +::: +:::::: + +:::::: {.d-flex .mt-3} +::: {.bg-teal-4 .color-swatch style="width: 160px; height: 40px;"} +.bg-teal-4 +::: +::: {.bg-teal-3 .color-swatch style="width: 160px; height: 40px;"} +.bg-teal-3 +::: +::: {.bg-teal-2 .color-swatch style="width: 160px; height: 40px;"} +.bg-teal-2 +::: +::: {.bg-teal-1 .color-swatch style="width: 160px; height: 40px;"} +.bg-teal-1 +::: +:::::: + +:::::: {.d-flex .mt-3} +::: {.bg-r-blue-2 .color-swatch .text-white style="width: 160px; height: 40px;"} +.bg-r-blue-2 +::: +::: {.bg-r-blue-1 .color-swatch .text-white style="width: 160px; height: 40px;"} +.bg-r-blue-1 +::: +:::::: + +### Text colors + +[.text-dark-blue-4]{.text-dark-blue-4}, [.text-dark-blue-3]{.text-dark-blue-3}, [.text-dark-blue-2]{.text-dark-blue-2}, [.text-dark-blue-1]{.text-dark-blue-1} + +[.text-orange-4]{.text-orange-4}, [.text-orange-3]{.text-orange-3}, [.text-orange-2]{.text-orange-2}, [.text-orange-1]{.text-orange-1} + +[.text-blue-4]{.text-blue-4}, [.text-blue-3]{.text-blue-3}, [.text-blue-2]{.text-blue-2}, [.text-blue-1]{.text-blue-1} + +[.text-silver-4]{.text-silver-4}, [.text-silver-3]{.text-silver-3}, [.text-silver-2]{.text-silver-2}, [.text-silver-1]{.text-silver-1} + +[.text-teal-4]{.text-teal-4}, [.text-teal-3]{.text-teal-3}, [.text-teal-2]{.text-teal-2}, [.text-teal-1]{.text-teal-1} + +[.text-r-blue-2]{.text-r-blue-2}, [.text-r-blue-1]{.text-r-blue-1} + +### Semantic colors + +[Primary](#){.btn .btn-primary .me-1} +[Secondary](#){.btn .btn-secondary .me-1} +[Success](#){.btn .btn-success .me-1} +[Danger](#){.btn .btn-danger .me-1} +[Warning](#){.btn .btn-warning .me-1} +[Info](#){.btn .btn-info .me-1} +[Light](#){.btn .btn-light .me-1} +[Dark](#){.btn .btn-dark .me-1} + +These will also apply to [Quarto callouts](https://quarto.org/docs/authoring/callouts.html). + +::: callout-note +Note that there are five types of callouts, including: `note`, `tip`, `warning`, +`caution`, and `important`. +::: + +::: callout-warning +Callouts provide a simple way to attract attention, for example, to this warning. +::: + +::: callout-important +The callout heading is provided by the callout type, with the expected heading +(i.e., Note, Warning, Important, Tip, or Caution). +::: + +::: callout-tip +## Tip With Title + +This is an example of a callout with a title. Providing a callout heading is +optional. +::: + +::: {.callout-caution collapse="true"} +## Caution: Expand To Learn About Collapse + +This is an example of a 'collapsed' caution callout that can be expanded by the +user. You can use `collapse="true"` to collapse it by default or `collapse="false"` +to make a collapsible callout that is expanded by default. +::: + +And they have utility classes for text colors: + +[.text-primary]{.text-primary}, [.text-primary-emphasis]{.text-primary-emphasis}, +[.text-secondary]{.text-secondary}, [.text-secondary-emphasis]{.text-secondary-emphasis}, +[.text-success]{.text-success}, [.text-success-emphasis]{.text-success-emphasis}, +[.text-danger]{.text-danger}, [.text-danger-emphasis]{.text-danger-emphasis}, +[.text-warning]{.text-warning}, [.text-warning-emphasis]{.text-warning-emphasis}, +[.text-info]{.text-info}, [.text-info-emphasis]{.text-info-emphasis}, +[.text-light]{.text-light}, [.text-light-emphasis]{.text-light-emphasis}, +[.text-dark]{.text-dark}, [.text-dark-emphasis]{.text-dark-emphasis}, +[.text-body]{.text-body}, [.text-body-emphasis]{.text-body-emphasis}, +[.text-body-secondary]{.text-body-secondary}, [.text-body-tertiary]{.text-body-tertiary} + +## Fonts + +Our fonts are a mix of Rust Foundation fonts (Noto Sans and Serif) and IBM's +Plex Mono. The monofont is typically used for headings and navbar, though Noto +Serif may also be used. For body text, we use Noto Sans. Utility classes are +provided for each, though they should rarely be needed. + +:::::: {.d-flex .gap-2 .small} +::: {.bg-dark-blue-4 .text-auto-dark .font-mono .p-3 .flex-fill} +[IBM Plex Mono]{.fs-3} +.font-mono +ABCDEFGHIJKLMNOPQRSTUVWXYZ +abcdefghijklmnopqrstuvwxyz +0123456789 +::: + +::: {.bg-dark-blue-3 .text-white .font-serif .p-3 .flex-fill} +[Noto Serif]{.fs-3} +.font-serif +ABCDEFGHIJKLMNOPQRSTUVWXYZ +abcdefghijklmnopqrstuvwxyz +0123456789 +::: + +::: {.bg-dark-blue-2 .text-auto-light .font-sans .p-3 .flex-fill} +[Noto Sans]{.fs-3} +.font-sans +ABCDEFGHIJKLMNOPQRSTUVWXYZ +abcdefghijklmnopqrstuvwxyz +0123456789 +::: +:::::: + +## Light and Dark + +The `.bg-*` and `.text-*` utility classes automatically adapt to light and dark +mode by flipping the tint scale — so `.text-dark-blue-4` in light mode becomes +`.text-dark-blue-1` in dark mode, and `.text-dark-blue-1` in light mode becomes +`.text-dark-blue-4` in dark mode. This means you generally don't need to +think about dark mode when using these classes. If you need a color to stay +fixed regardless of the theme, however, simply append `-fixed` to the class +name: `.bg-orange-4-fixed` and `.text-orange-4-fixed` will always render the +original color. + +Here are some examples. To see how they work, toggle light and dark mode in the +browser: + +[.bg-orange-4]{.bg-orange-4} +[.bg-orange-4-fixed]{.bg-orange-4-fixed} + +Notice that the font changes from black to white in each example. This is +Bootstrap's color system adapting to the page background, which is meant to +ensure a certain contrast level for accessibility. Semantic colors (`.text-primary`, +`.btn-success`, callouts, etc.) are also handled by Bootstrap's color system and +do not need any special treatment. However, you will notice that Bootstrap's +color system does not recognize these utilities - currently, it does not even +recognize its own utilities! - so contrast can be lost when using them. To +ensure that accessibility is maintained, you may want to add `.text-auto-dark` +or `.text-auto-light` classes, too. These will ensure that white and black text +are correctly used with our utilities. + +[.bg-orange-1 with .text-auto-light]{.bg-orange-1 .text-auto-light} +[.bg-orange-4 with .text-auto-dark]{.bg-orange-4 .text-auto-dark} diff --git a/contributing/documenting-code.qmd b/contributing/documenting-code.qmd new file mode 100644 index 0000000..37ebdf5 --- /dev/null +++ b/contributing/documenting-code.qmd @@ -0,0 +1,63 @@ +--- +title: "Documenting Code" +engine: knitr +--- + +Code in extendr and rextendr should be thoroughly documented using widely +accepted styling conventions in the R and Rust communities. + +## Writing style + +- Be concise! And stay on topic! The reader should not have to wade through + lengthy digressions to get to the point. +- Eschew obfuscation and avoid needlessly wordy phrases like 'eschew + obfuscation.' In other words, keep it simple. +- Second person ("you") is fine for instructions, but do not write about the + reader's intentions in the second person (avoid expressions like "you want to" + or "you need to"). + + +## Rust Documentation + +All Rust code in extendr should be documented with +[rustdoc](https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html) +following conventions recommended in the +[Rust API Guidelines](https://rust-lang.github.io/api-guidelines/about.html). + +Doc examples that call the R API must be wrapped in `with_r`. Bare doctests +without it will fail at runtime. + +```rust +/// ``` +/// use extendr_api::prelude::*; +/// with_r(|| { +/// let x = r!([1, 2, 3]); +/// assert_eq!(x.len(), 3); +/// Ok(()) +/// }); +/// ``` +``` + +## R Documentation + +Functions in rextendr should be documented with [roxygen2](https://roxygen2.r-lib.org), +following the conventions recommended by the [tidyverse style guide](https://style.tidyverse.org/documentation.html). + +- Roxygen documentation for every exported function must include the following + roclets: + - `@title` + - `@description` — a concise description of what the function does. + - `@param` — one tag per parameter, specifying: + - the data type (e.g. `integer`, `character`, `logical`) + - whether it is a scalar or vector + - the default value, if one exists + - `@return` — what the function returns, including the R type. + - `@examples` +- The `@details` section are optional, but should be included for most functions + in rextendr. +- Internal functions should include roxygen comments if their use in rextendr is + fairly complex, important, and not easy to discern. Simple helper functions do + not need to be documented. +- In vignettes and online guides, use the `extendrsrc` and `extendr` knitr + engines for examples involving Rust code. See `user-guide/serde-integration.qmd` + for a worked example to emulate. \ No newline at end of file diff --git a/contributing/extendr-internals.qmd b/contributing/extendr-internals.qmd new file mode 100644 index 0000000..e0cb11c --- /dev/null +++ b/contributing/extendr-internals.qmd @@ -0,0 +1,4 @@ +--- +title: "{extendr} Internals" +engine: knitr +--- \ No newline at end of file diff --git a/contributing/index.qmd b/contributing/index.qmd new file mode 100644 index 0000000..962f6fe --- /dev/null +++ b/contributing/index.qmd @@ -0,0 +1,36 @@ +--- +title: "Contributing" +page-layout: article +toc: true +toc-depth: 0 +date: today +date-format: long +--- + +The extendr project has three main areas where you can contribute: + +**extendr crates** — The core Rust library. This is where the `#[extendr]` +macro, R type bindings, and the Rust API live. +Contributions here require Rust experience. The repository is at +[github.com/extendr/extendr](https://github.com/extendr/extendr). + +**rextendr package** — The R package that scaffolds new extendr projects. +Contributions here require R package development experience. The repository is +at [github.com/extendr/rextendr](https://github.com/extendr/rextendr). + +**Documentation website** — The site you are reading now. Contributions +here range from fixing typos to writing new guides and tutorials. The +repository is at +[github.com/extendr/extendr.github.io](https://github.com/extendr/extendr.github.io). + +## Software requirements + +This guide assumes the following versions of necessary software: + +- R: {{< var version.r >}} +- Rust {{< var version.rust >}} +- extendr-api {{< var version.extendr >}} +- rextendr {{< var version.rextendr >}} + +Please see [Get Started](../get-started.qmd) if you have not already installed this +software. \ No newline at end of file diff --git a/contributing/rextendr-internals.qmd b/contributing/rextendr-internals.qmd new file mode 100644 index 0000000..6a6adba --- /dev/null +++ b/contributing/rextendr-internals.qmd @@ -0,0 +1,110 @@ +--- +title: "{rextendr} Internals" +--- + +This page describes various internal functions and processes used by rextendr to +setup and build R packages. It is designed to get contributors up-to-speed on +the internal mechanics of rextendr, so that they can more efficiently contribute +to development and maintenance. + +## Package Setup + +General R package setup is achieved with `usethis::create_package()`. +Scaffolding files required to call Rust code in R using extendr is then added to +the package directory with `rextendr::use_extendr()`. This involves +interpolating strings into template files in `inst/templates/`, which are then +written to the user's package directory via the internal function +`use_rextendr_template()`: + +```{.r filename="R/use_extendr.R"} +use_rextendr_template <- function( + template, # filename in inst/templates/ + save_as, # destination path in the user's package + data = list(), # named list of variables to interpolate into the template + quiet = FALSE, + overwrite = NULL +) +``` + +When `usethis` is available and `overwrite = NULL`, this delegates to +`usethis::use_template()`, which handles interactive prompting if the file +already exists. Otherwise it reads the template file directly, performs +interpolation, and writes the result. The templates directory currently includes +all of the following: + +```{r} +fs::dir_tree(system.file("templates", package = "rextendr")) +``` + +The exceptions to simple string interpolation are `Cargo.toml`, whose content is +generated programmatically before being written to file, and `Makevars.in` / +`Makevars.win.in`, which use a second layer of `@PLACEHOLDER@` substitution +performed at package build time by `tools/config.R`. + +You can see the content of these template files on main in the rextendr +repository here: {{< iconify bi:github >}} +[source](https://github.com/extendr/rextendr/tree/main/inst/templates). +For a description of what each generated file does, see the +[Project Structure](../user-guide/package-structure.qmd) page in the user guide. + +## Package Build + +When a developer calls `devtools::document()`, they initiate a set of steps to +build, document, and register functions in their R package. Here are those steps +in order: + +1. Check Rust and cargo versions +2. Generate `Makevars` from template +3. Compile the Rust crate into a static library +4. Generate R wrappers +5. Compile `entrypoint.c` and link the static library +6. Register exported routines with R on load +7. Clean up build artifacts + +These steps are driven by `R CMD INSTALL`, which `devtools::document()` calls +implicitly. + +### Setup + +The first thing that gets called is `configure` (`configure.win` on Windows). +The sole purpose of the configure script is to source the R script +`tools/config.R`, which in broad outline does two things. First, it runs +`tools/msrv.R` to check that the Rust version is consistent across metadata +files like `DESCRIPTION` and `Cargo.toml` (step 1). Second, it substitutes +`@PLACEHOLDER@` variables in the `Makevars.in` and `Makevars.win.in` templates +(signaled by `.in`) with relevant data to generate the actual Makevars file +(step 2). + +### Build + +The R package build process next invokes Makevars, which was just generated by +the configuration file. The Makevars in turn does two things. First, it calls +`cargo build` to compile the static library (step 3). Then it calls `cargo run` +to compile and execute the associated `document` binary, which generates all +extendr wrappers (basically `.Call()`) and writes them into +`R/extendr-wrappers.R` (step 4). Note that if a `vendor/` directory or +`rust/vendor.tar.xz` tarball is present, the Makevars also ensures that Cargo is +configured to use it for offline compilation — this is the essential mechanism +required for CRAN submissions. + +### Registeration + +Because we are building an R package with compiled code, R requires that we +register all routines via a C-level initialization function named +`R_init_`. This is where `entrypoint.c` comes in. Currently, extendr +generates its own version of this function — `R_init_{{{mod_name}}}_extendr` — +inside the Rust library. The C entrypoint then bridges the two, calling the +Rust-generated function through the R-facing `R_init_{{{mod_name}}}`. Compiling +`entrypoint.c` makes that symbol available on package load (step 5). When the +package next gets loaded, the C function is called to register compiled routines +(step 6). If you inspect `R/extendr-wrappers.R`, you will see where this +happens. The call is `@useDynLib(pkgname, .registration = TRUE)`. + +### Cleanup + +To prevent the `Makevars` generated by `configure` from being committed to git, +a `cleanup` is invoked (`cleanup.win` on Windows) that calls the shell command +`rm` to remove that file (step 7). + +Once these steps are taken, the compiled Rust should become available in the +current R session. diff --git a/contributing/testing-code.qmd b/contributing/testing-code.qmd new file mode 100644 index 0000000..0d25db5 --- /dev/null +++ b/contributing/testing-code.qmd @@ -0,0 +1,41 @@ +--- +title: "Testing Code" +engine: knitr +--- + +## Rust Tests + +Rust tests in extendr follow standard Rust conventions: `#[test]` functions +in `#[cfg(test)]` modules, either inline in the source file or in a separate +file under `tests/`. For details, see [Writing Automated Tests](https://doc.rust-lang.org/book/ch11-00-testing.html) +in The Book. + + + +### Starting an R session in tests + +An important wrinkle of testing Rust code in extendr is that most tests need a +live R session. In normal package use, R is already running when Rust code +executes. But when running `cargo test` directly, there is no R session — so any +test that touches an R object must start one first. + +Use the `test!` macro to wrap test code that calls the R API. It starts R and +allows `?` inside the block: + +```rust +#[test] +fn my_test() { + test! { + let x = r!([1, 2, 3]); + assert_eq!(x.len(), 3); + } +} +``` + +If a test does not call the R API at all — for example, testing pure Rust +arithmetic on scalar wrapper types — `test!` is not needed. + +## R Tests + +R code in rextendr is tested with [testthat](https://testthat.r-lib.org), +following standard R package testing conventions. diff --git a/contributing/user-guide-pages.qmd b/contributing/user-guide-pages.qmd new file mode 100644 index 0000000..cb4c23d --- /dev/null +++ b/contributing/user-guide-pages.qmd @@ -0,0 +1,50 @@ +--- +title: "User Guide Pages" +engine: knitr +--- + +User guide pages are friendly introductions to specific features of the +`extendr-api` crate. Keep them **concise** and use **simple language**. +Always use code chunks for extendr code — never paste raw snippets. + +### Page structure + +Each page should follow this rough structure: + +````markdown +--- +title: "An informative title" +--- + +- Motivate the functionality the page covers +- No more than 4 sentences + +## The feature + +- Describe the functionality +- Discuss prerequisites if any + +## Basic usage + +- Discuss the code before showing it + +```{extendrsrc} +// your Rust code +``` + +- Summarize and explain the output + +## Advanced usage + +- Motivate the advanced example + +```{extendrsrc} +// your Rust code +``` + +- Summarize and explain + +## See also + +- Links to related pages or external docs +```` \ No newline at end of file diff --git a/contributing/writing-code.qmd b/contributing/writing-code.qmd new file mode 100644 index 0000000..5c368ad --- /dev/null +++ b/contributing/writing-code.qmd @@ -0,0 +1,46 @@ +--- +title: "Writing Code" +engine: knitr +--- + +This page covers code formatting conventions for the extendr project. Following +these conventions keeps code consistent and readable across contributors. + +## Rust Code + +Rust code should be written to conform with conventions outlined in the +[Rust API Guidelines](https://rust-lang.github.io/api-guidelines/about.html). +It's a good idea to read this document, but it may take time to gain some +familiarity with all of its ins and outs. In the meantime, the following tools +will prove very beneficial to keeping your Rust code consistent with those +conventions. + +- Use [rustfmt](https://rust-lang.github.io/rustfmt/?version=v1.9.0) to + format Rust code. rustfmt is the standard Rust formatter and can be + configured to run on save in most editors. + +- Use [Clippy](https://doc.rust-lang.org/stable/clippy/) to lint Rust code. + Clippy catches common mistakes and enforces idiomatic Rust style. + +- Use [rust-analyzer](https://rust-analyzer.github.io/) for IDE support. + It provides code completion, inline errors, and other features in VS Code, + Positron, Zed, and other editors. + +## R Code + +R code should be written to conform with conventions outlined in the +[Tidyverse style guide](https://style.tidyverse.org/). What we said about the +Rust API guidelines, it applies here, too. In your effort to abide by those +conventions, you will find a great deal of advantage in these tools. + +- Use [Air](https://posit-dev.github.io/air/) to format R code. Air can be + configured to automatically format R scripts on save in your editor. Air + supports VS Code, Positron, Zed, and the command line. + +- Use [lintr](https://lintr.r-lib.org/) to lint R code. rextendr's CI uses + lintr, so having it configured in your editor will catch issues before they + are flagged in review. lintr supports most IDEs, include RStudio, VS Code, and + Positron. You may also like [jarl](https://jarl.etiennebacher.com/), an R + lintr written in Rust, which is much faster than lintr and supports additional + IDEs. + diff --git a/css/_bootswatch.scss b/css/_bootswatch.scss deleted file mode 100644 index 1228e94..0000000 --- a/css/_bootswatch.scss +++ /dev/null @@ -1,642 +0,0 @@ -/*-- scss:defaults --*/ -// Brite 5.3.7 -// Bootswatch - -$theme: "brite" !default; - -// -// Color system -// - -$white: #fff !default; -$gray-100: #f8f9fa !default; -$gray-200: #e9ecef !default; -$gray-300: #dee2e6 !default; -$gray-400: #ced4da !default; -$gray-500: #adb5bd !default; -$gray-600: #868e96 !default; -$gray-700: #495057 !default; -$gray-800: #343a40 !default; -$gray-900: #212529 !default; -$black: #000 !default; - -$blue: #61bcff !default; -$indigo: #828df9 !default; -$purple: #be82fa !default; -$pink: #ea4998 !default; -$red: #f56565 !default; -$orange: #fa984a !default; -$yellow: #ffc700 !default; -$green: #68d391 !default; -$teal: #2ed3be !default; -$cyan: #22d2ed !default; -$lime: #a2e436 !default; - -$extendrblue: #7fc6ff; - -// $primary: $lime !default; -$primary: $extendrblue !default; -$secondary: $white !default; -$success: $green !default; -$info: $cyan !default; -$warning: $yellow !default; -$danger: $red !default; -$light: $gray-200 !default; -$dark: $black !default; - -// $min-contrast-ratio: 1.75 !default; - -$spacer: 1rem !default; - -// Body - -// Links - -$link-color: $black !default; - -// Components - -$border-radius: .375rem !default; -$border-radius-sm: .25rem !default; -$border-radius-lg: .5rem !default; - -$component-active-color: $black !default; -// $component-active-bg: $black !default; - -$focus-ring-width: 1px !default; -$focus-ring-opacity: 1 !default; -$focus-ring-color: $black !default; - -// Fonts - -$font-size-base: .875rem !default; -$font-size-sm: $font-size-base * .875 !default; - -$lead-font-weight: 400 !default; - -$headings-font-weight: 500 !default; - -$hr-border-width: 2px !default; -$hr-opacity: 1 !default; - -// Tables - -$table-cell-padding-y: .75rem !default; -$table-cell-padding-x: .75rem !default; - -$table-border-width: 1px !default; -$table-border-color: $black !default; - -$table-hover-bg-factor: 0 !default; - -$table-border-factor: 1 !default; - -$table-bg-scale: 0% !default; - -$table-variants: ( - "primary": $primary, - "secondary": $secondary, - "success": $success, - "info": $info, - "warning": $warning, - "danger": $danger, - "light": $light, - "dark": $dark, -) !default; - -// Buttons + Forms - -$input-btn-padding-y: .5rem !default; -$input-btn-padding-x: 1rem !default; - -$input-btn-padding-y-sm: .25rem !default; -$input-btn-padding-x-sm: .75rem !default; -$input-btn-font-size-sm: $font-size-sm !default; - -$input-btn-padding-y-lg: .75rem !default; -$input-btn-padding-x-lg: 1.25rem !default; -$input-btn-font-size-lg: $font-size-base !default; - -$input-btn-border-width: 2px !default; - -// Buttons - -$btn-border-width: 2px !default; - -// Forms - -$form-label-font-weight: $headings-font-weight !default; - -$input-focus-border-color: $black !default; - -$form-check-input-checked-border-color: $black !default; - -$form-range-track-height: .6rem !default; - -$form-range-thumb-width: 1.2rem !default; -$form-range-thumb-height: $form-range-thumb-width !default; -$form-range-thumb-border: 2px solid $black !default; -$form-range-thumb-box-shadow: none !default; -$form-range-thumb-disabled-bg: $light !default; - -// Dropdowns - -// $dropdown-link-hover-color: $white !default; -// $dropdown-link-hover-bg: $primary !default; - -// Navs - -$nav-link-color: $black !default; -$nav-link-hover-color: $black !default; - -$nav-tabs-link-hover-border-color: transparent !default; - -// Navbar - -$navbar-padding-y: $spacer * .75 !default; -$navbar-padding-x: null !default; - -$navbar-nav-link-padding-x: 1rem !default; - -$navbar-light-color: $black !default; -$navbar-light-hover-color: $black !default; -$navbar-light-active-color: $black !default; -$navbar-light-disabled-color: rgba($black, .3) !default; -$navbar-light-icon-color: $black !default; -$navbar-light-toggler-border-color: $black !default; - -$navbar-dark-color: $white !default; -$navbar-dark-hover-color: $white !default; -$navbar-dark-active-color: $white !default; -$navbar-dark-disabled-color: rgba($white, .3) !default; -$navbar-dark-icon-color: $white !default; -$navbar-dark-toggler-border-color: $white !default; - -// Dropdowns - -$dropdown-border-color: $black !default; -$dropdown-border-width: 2px !default; - -$dropdown-link-hover-bg: $primary !default; - -$dropdown-header-color: $black !default; - -// Pagination - -$pagination-color: $black !default; -$pagination-hover-color: $black !default; -$pagination-hover-bg: transparent !default; -$pagination-active-border-color: $black !default; - -// Cards - -$card-border-color: $black !default; - -// Accordion - -$accordion-button-active-bg: $primary !default; -$accordion-button-active-color: $black !default; - -// Tooltips - -$tooltip-opacity: 1 !default; - -// Popovers - -$popover-border-color: $black !default; - -// Toasts - -$toast-border-color: $black !default; -$toast-box-shadow: 3px 3px 0 0 $black !default; - -$toast-header-color: $black !default; - -// Badges - -$badge-color: $black !default; - -// Modals - -$modal-content-border-color: $black !default; - -// Alerts - -// List group - -$list-group-active-border-color: $black !default; - -// Breadcrumbs - -$breadcrumb-padding-y: .5rem !default; -$breadcrumb-padding-x: 1rem !default; -$breadcrumb-divider-color: $black !default; -$breadcrumb-active-color: $black !default; -$breadcrumb-border-radius: $border-radius-lg !default; - -// Close - -$btn-close-opacity: 1 !default; -$btn-close-hover-opacity: 1 !default; - -/*-- scss:rules --*/ -// Brite 5.3.7 -// Bootswatch - - -// Variables - -:root { - --#{$prefix}border-width: 2px; - --#{$prefix}border-color: #000; -} - -[data-bs-theme="light"], -[data-bs-theme="dark"] { - --#{$prefix}border-color: #000; -} - -/*-- scss:mixins --*/ - -@mixin btn-shadow(){ - box-shadow: 3px 3px 0 0 var(--#{$prefix}border-color); -} - -table, -.table { - border: 2px solid $black !important; -} - -// Navbar - -.navbar { - $navbar-border-width: 2px; - font-size: $font-size-lg; - font-weight: $headings-font-weight; - - border: $navbar-border-width solid $black; - - &.fixed-top { - border-width: 0 0 $navbar-border-width 0; - } - - &.fixed-bottom { - border-width: $navbar-border-width 0 0 0; - } - - .dropdown-toggle::after { - margin-left: .15em; - vertical-align: .15em; - } - - &-toggler { - --bs-border-width: 2px; - } -} - -// #quarto-header, -// #quarto-header nav:first-child { -// background-color: var(--bs-light); -// } - -.navbar-title { - font-weight: 600; - font-family: monospace; -} - -.sourceCode { - @extend .border-dark; -} - -p code { - background-color: rgba(var(--bs-light-rgb), 0.1); -} - -// Buttons - -.btn { - margin: 3px 0 0 3px; - border-color: $black; - @include btn-shadow(); - transition: all .3s; - transform: translate(-3px, -3px); - - &:hover { - border-color: $black; - box-shadow: none; - transform: translate(0, 0); - } - - &.disabled { - border-color: $black; - } - - &-link, - &-link:hover { - color: $black; - } -} - -@each $color, $value in $theme-colors { - .btn-outline-#{$color} { - color: $black; - background-color: #{$value}; - box-shadow: none; - transform: translate(0, 0); - - &:hover { - @include btn-shadow(); - transform: translate(-3px, -3px); - } - } - - .btn-check + .btn-#{$color}, - .btn-check + .btn-outline-#{$color} { - &:hover { - background-color: #{$value}; - } - } -} - -.btn-outline-dark { - color: $white; -} - -.btn-group { - .btn { - margin-left: 0; - } - - > .btn.active { - z-index: 0; - } -} - -.btn-group, -.btn-group-vertical { - .btn { - &:hover, - &:active, - &:focus { - z-index: 0; - } - - } - - > .btn-check:focus + .btn, - > .btn-check:checked + .btn { - z-index: 0; - } -} - -.btn-check + .btn { - border-color: $black; - @include btn-shadow(); - transform: translate(-3px, -3px); -} - -.btn-check + .btn:hover { - color: $black; - border-color: $black; -} - -.btn-check:checked + .btn, -:not(.btn-check) + .btn:active, -.btn:first-child:active, -.btn.active, -.btn.show { - border-color: $black; - box-shadow: none; - transform: translate(0, 0); -} - -[data-bs-theme="dark"] .btn-link, -[data-bs-theme="dark"] .btn-link:hover { - color: #fff; -} - -// Typography - -a { - font-weight: $headings-font-weight; -} - -// Forms - -.form-range { - - &:not([disabled])::-moz-range-progress { - height: .6rem; - background-color: $primary; - border-radius: 6px 0 0 6px; - } - - &:not([disabled])::-ms-fill-lower { - height: .6rem; - background-color: $primary; - border-radius: 6px 0 0 6px; - } - - &::-webkit-slider-runnable-track { - border: 2px solid $black; - } - - &::-moz-range-track { - border: 2px solid $black; - } - - &::-webkit-slider-thumb { - margin-top: -.4rem; - } - - &::-moz-range-thumb { - margin-top: -.4rem; - } - - &:focus::-webkit-slider-thumb { - box-shadow: none; - } - - &:focus::-moz-range-thumb { - box-shadow: none; - } -} - -// Navs - -.nav { - &:not(.nav-tabs, .nav-pills) { - .nav-link { - &.active { - font-weight: 700; - } - } - } -} - -.nav-tabs { - gap: 4px; - padding: .4rem .4rem calc(.4rem + 2px); - font-weight: $headings-font-weight; - border: 2px solid $black; - border-radius: $border-radius-lg; - - .nav-link { - border-width: 2px; - border-radius: $border-radius; - - &.active, - &:hover { - border: 2px solid $black; - } - } -} - -.tab-content { - padding: 1rem; - margin-top: 1rem; - border: 2px solid $black; - border-radius: $border-radius-lg; -} - -.nav-pills { - gap: 4px; - font-weight: $headings-font-weight; - - .nav-link { - border: 2px solid $black; - } -} - -.breadcrumb { - font-weight: $headings-font-weight; - border: 2px solid $black; - - a { - color: $black; - } -} - -.dropdown-menu { - @include btn-shadow(); - border-radius: $border-radius; -} - -.dropdown-header { - font-weight: 700; -} - -.dropdown-item:hover, -.dropdown-item:focus { - color: #000; -} - -@each $color, $value in $theme-colors { - .list-group-item-#{$color} { - color: $black; - background-color: #{$value}; - border-color: $black; - } -} - -.list-group-item-dark { - color: $white; -} - -[data-bs-theme="dark"] { - .nav-tabs, - .nav-pills { - .nav-link { - --#{$prefix}nav-link-color: #fff; - - &:hover, - &:focus { - --#{$prefix}nav-link-hover-color: #fff; - } - } - } - - .page-link { - --#{$prefix}pagination-color: #fff; - - &:hover { - --#{$prefix}pagination-hover-color: #fff; - } - } - - .nav { - .nav-link { - --#{$prefix}nav-link-color: #fff; - - &:hover, - &:focus { - --#{$prefix}nav-link-hover-color: #fff; - } - } - } - - .breadcrumb { - a { - color: $white; - } - } - - .breadcrumb-item { - &.active { - --#{$prefix}breadcrumb-item-active-color: #fff; - } - } -} - -// Indicators - -.alert { - color: $black; - border: 2px solid $black; - @include btn-shadow(); -} - -.alert-link { - color: $black; -} - -.alert-dark { - color: $white; -} - -@each $color, $value in $theme-colors { - .alert-#{$color} { - background-color: #{$value}; - } -} - -.badge { - border: 2px solid $black; - - &.bg-dark { - color: $white; - } -} - -.progress { - border: 2px solid $black; - - &-bar { - border-right: 2px solid $black; - } -} - -.modal { - &-content { - @include btn-shadow(); - } -} - -.vr { - width: 2px; - background-color: $black; -} - -[data-bs-theme="dark"] .toast-header { - --#{$prefix}toast-header-color: #fff; -} diff --git a/css/extendr.scss b/css/extendr.scss new file mode 100644 index 0000000..cebb0e4 --- /dev/null +++ b/css/extendr.scss @@ -0,0 +1,354 @@ +/*-- scss:uses --*/ +@use "sass:color"; +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Noto+Serif:ital,wght@0,100..900;1,100..900&display=swap'); + +/*-- scss:defaults --*/ +$min-contrast-ratio: 4.5 !default; + +// bootstrap grays +$white: #fff !default; +$gray-100: #f8f9fa !default; +$gray-200: #ecf0f1 !default; +$gray-300: #dee2e6 !default; +$gray-400: #ced4da !default; +$gray-500: #b4bcc2 !default; +$gray-600: #95a5a6 !default; +$gray-700: #7b8a8b !default; +$gray-800: #343a40 !default; +$gray-900: #212529 !default; +$black: #000 !default; + +// base colors +$true-r-blue-1: #276DC3; +$true-r-blue-2: #165CAA; +$desaturated-r-blue-1: #586D99; +$desaturated-r-blue-2: #1A2740; +$rust-orange: #D34516; +$rust-silver: #67737A; +$flatly-yellow: #f39c12; + +// base extendr colors +// the blues are desaturated versions of the original r blues +// tint scales (4 = original, 1 = 75% tinted) +$dark-blue-4: $desaturated-r-blue-2; +$dark-blue-3: color.mix(white, $dark-blue-4, 25%, $method: oklch); +$dark-blue-2: color.mix(white, $dark-blue-4, 50%, $method: oklch); +$dark-blue-1: color.mix(white, $dark-blue-4, 75%, $method: oklch); + +$orange-4: $rust-orange; +$orange-3: color.mix(white, $orange-4, 25%, $method: oklch); +$orange-2: color.mix(white, $orange-4, 50%, $method: oklch); +$orange-1: color.mix(white, $orange-4, 75%, $method: oklch); + +$blue-4: $desaturated-r-blue-1; +$blue-3: color.mix(white, $blue-4, 25%, $method: oklch); +$blue-2: color.mix(white, $blue-4, 50%, $method: oklch); +$blue-1: color.mix(white, $blue-4, 75%, $method: oklch); + +$silver-4: $rust-silver; +$silver-3: color.mix(white, $silver-4, 25%, $method: oklch); +$silver-2: color.mix(white, $silver-4, 50%, $method: oklch); +$silver-1: color.mix(white, $silver-4, 75%, $method: oklch); + +$teal-4: #96C5B0; +$teal-3: color.mix(white, $teal-4, 25%, $method: oklch); +$teal-2: color.mix(white, $teal-4, 50%, $method: oklch); +$teal-1: color.mix(white, $teal-4, 75%, $method: oklch); + +// semantic colors +$primary: $true-r-blue-2; +$secondary: $silver-4; +$success: $teal-4; +$danger: $orange-4; +$warning: $flatly-yellow; +$info: $true-r-blue-1; +$light: $gray-200; +$dark: $gray-800; + +// quarto callouts +$callout-color-note: $primary; +$callout-color-important: $danger; +$callout-color-tip: $success; +$callout-color-caution: color.mix($warning, $danger, 50%, $method: oklch); +$callout-color-warning: $warning; + +$link-color: color.mix(black, $teal-4, 20%, $method: oklch); +$link-hover-color: color.mix(black, $teal-4, 40%, $method: oklch); +$nav-link-color: $link-color; +$nav-link-hover-color: $link-hover-color; +$navbar-bg: white; +$navbar-fg: black; + +// fonts +$font-family-mono: "IBM Plex Mono", "Courier New", monospace; +$font-family-serif: "Noto Serif", Georgia, serif; +$font-family-sans-serif: "Noto Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default; +$headings-font-family: $font-family-mono; + + +/*-- scss:rules --*/ +$palette: ( + 'dark-blue': ($dark-blue-1, $dark-blue-2, $dark-blue-3, $dark-blue-4), + 'orange': ($orange-1, $orange-2, $orange-3, $orange-4), + 'blue': ($blue-1, $blue-2, $blue-3, $blue-4), + 'silver': ($silver-1, $silver-2, $silver-3, $silver-4), + 'teal': ($teal-1, $teal-2, $teal-3, $teal-4), +); + +// define css custom properties for all palette colors +:root { + @each $name, $tints in $palette { + @for $i from 1 through 4 { + --#{$name}-#{$i}: #{nth($tints, $i)}; + } + } + + --r-blue-1: #{$true-r-blue-1}; + --r-blue-2: #{$true-r-blue-2}; + + --font-mono: #{$font-family-mono}; + --font-serif: #{$font-family-serif}; + --font-sans: #{$font-family-sans-serif}; +} + +// generate utility classes using css custom properties +@each $name, $tints in $palette { + @for $i from 1 through 4 { + .bg-#{$name}-#{$i} { + background-color: var(--#{$name}-#{$i}); + } + + .text-#{$name}-#{$i} { + color: var(--#{$name}-#{$i}); + } + + .bg-#{$name}-#{$i}-fixed { + background-color: nth($tints, $i); + } + + .text-#{$name}-#{$i}-fixed { + color: nth($tints, $i); + } + } +} + +.bg-r-blue-1 { + background-color: $true-r-blue-1; +} + +.bg-r-blue-2 { + background-color: $true-r-blue-2; +} + +.text-r-blue-1 { + color: $true-r-blue-1; +} + +.text-r-blue-2 { + color: $true-r-blue-2; +} + +.font-sans { + font-family: $font-family-sans-serif; +} + +.font-serif { + font-family: $font-family-serif; +} + +.font-mono { + font-family: $font-family-mono; +} + +.navbar { + border-radius: 0; + font-family: $font-family-mono; + + .nav-link, + .navbar-brand { + color: black; + } + + .navbar-brand { + font-weight: 500; + font-size: 2rem; + } + + .nav-link:hover { + color: $dark-blue-4; + } + + .dropdown-menu { + --bs-dropdown-link-color: #{$silver-4}; + } + + .navbar-container:has(#navbarCollapse) #quarto-search { + margin-left: 0; + } +} + +@media (max-width: 991px) { + #navbarCollapse .navbar-nav { + flex-direction: row; + flex-wrap: wrap; + + .nav-item:not(.compact) { + display: block; + width: 100%; + } + } +} + +.sidebar-link.active { + color: $link-color; +} + +.card-img-col img { + width: 120px !important; + max-width: none !important; + object-fit: contain; + + @media (max-width: 576px) { + width: 92px !important; + } +} + +.guide-header { + border-bottom: 1px solid $gray-300; + margin-bottom: 1.5rem; + padding-bottom: 1rem; +} + + +.card { + + h1:first-child, + h2:first-child, + h3:first-child, + h4:first-child, + h5:first-child, + h6:first-child { + margin-top: 0; + } + + p:last-child { + margin-bottom: 0; + } +} + + +.text-auto-dark { + color: white; +} + +.text-auto-light { + color: black; +} + +nav.page-navigation a.pagination-link:hover { + color: var(--bs-link-hover-color); +} + +.quarto-title-breadcrumbs .breadcrumb { + margin-bottom: 0.5em; + font-size: 0.8rem; + padding-left: 0; + --bs-breadcrumb-bg: ; +} + +// dark mode +body.quarto-dark { + + // swap tint scale (1↔4, 2↔3) + @each $name, $tints in $palette { + @for $i from 1 through 4 { + --#{$name}-#{$i}: #{nth($tints, 5 - $i)}; + } + } + + --bs-link-color: #{$teal-4}; + --bs-link-color-rgb: #{color.red($teal-4)}, + #{color.green($teal-4)}, + #{color.blue($teal-4)}; + --bs-link-hover-color: #{$teal-2}; + --bs-link-hover-color-rgb: #{color.red($teal-2)}, + #{color.green($teal-2)}, + #{color.blue($teal-2)}; + + .nav-link { + --bs-nav-link-color: #{$teal-4}; + } + + .sidebar nav[role=doc-toc] ul>li>a:hover, + .sidebar nav[role=doc-toc] ul>li>ul>li>a:hover { + color: #{$teal-2} !important; + } + + .sidebar-link:hover { + color: $teal-2; + } + + .sidebar-link.active { + color: $teal-4; + + &:hover { + color: $teal-2; + } + } + + .navbar { + background-color: var(--bs-body-bg); + border: none; + color: white; + + .nav-link, + .navbar-brand { + color: white; + } + + .nav-link:hover { + color: $dark-blue-1; + } + + .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon .aa-SubmitIcon { + color: white !important; + } + + .quarto-color-scheme-toggle .bi { + filter: invert(1); + } + + .dropdown-menu { + --bs-dropdown-link-color: #{$silver-1}; + } + } + + .guide-header { + border-bottom-color: $gray-700; + } + + + .text-auto-dark { + color: black; + } + + .text-auto-light { + color: white; + } + + .callout-icon { + filter: brightness(5); + } + + h1, + h2, + h3, + h4, + h5, + h6 { + code { + background-color: rgba(255, 255, 255, 0.1); + color: inherit; + } + } +} \ No newline at end of file diff --git a/css/index.css b/css/index.css new file mode 100644 index 0000000..23e655e --- /dev/null +++ b/css/index.css @@ -0,0 +1,99 @@ +#quarto-content { + padding-left: 1em; + padding-right: 1em; +} + +.hero-banner { + padding-left: 0.75rem; + padding-right: 0.75rem; + max-width: 100%; +} + + +#hero-header { + font-family: var(--font-mono); + color: var(--silver-4); +} + +#hero-header h1 { + font-family: var(--font-sans); + color: black; + font-weight: 500; +} + +body.quarto-dark #hero-header h1 { + color: white; +} + +#hero-header p { + font-size: 0.9rem; + margin-bottom: 0; +} + +@media (min-width: 992px) { + .hero-banner { + max-width: 50%; + } + + #hero-header p { + font-size: 1.1rem; + } + + #hero-header h1 { + font-size: 3.8rem; + } +} + +#hero-cli { + margin-top: 3em; +} + +/* tab buttons look like shell commands */ +#hero-cli .nav-tabs { + border-bottom: none; + gap: 0.25rem; +} + +#hero-cli .nav-tabs .nav-link { + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--silver-4); + background: none; + border: 1px solid transparent; + border-radius: 0; + padding: 0.25rem 0.5rem; +} + +#hero-cli .nav-tabs .nav-link::before { + content: "$ "; + color: var(--orange-4); +} + +#hero-cli .nav-tabs .nav-link:hover { + color: var(--dark-blue-4); + border-color: transparent; +} + +#hero-cli .nav-tabs .nav-link.active { + color: var(--dark-blue-4); + font-weight: 600; + background: none; + border-color: transparent; + border-bottom: 2px solid var(--orange-4); +} + +/* tab content looks like terminal output */ +#hero-cli .tab-content { + font-family: var(--font-mono); + font-size: 0.9rem; + background-color: var(--bs-gray-100); + border: none; + border-left: 3px solid var(--orange-4); + padding: 1rem 1.25rem; + margin-top: 0.5rem; +} + +/* dark mode */ +body.quarto-dark #hero-cli .tab-content { + background-color: var(--bs-gray-800); +} \ No newline at end of file diff --git a/get-started.qmd b/get-started.qmd index c2b3397..a2eb0f0 100644 --- a/get-started.qmd +++ b/get-started.qmd @@ -3,48 +3,81 @@ title: "Get Started" toc: false --- -To build R packages with **extendr**, you need to have the right tools. - -## Step 1 [Install Rust]{.fw-light} - -::: clearfix -::: {.float-start style="width: 126px; margin-right: 0.8rem;"} -[![](images/cuddlyferris.svg)](https://www.rust-lang.org/tools/install) +::::::: card +:::::: {.row .g-0 .d-flex .flex-column .flex-md-row} +::: {.col-md-4 .d-flex .align-items-center .justify-content-center .p-3 .card-img-col} +[![](images/cuddlyferris.svg){width="96"}](https://www.rust-lang.org/tools/install) ::: +:::: col-md-8 +::: card-body +### Step 1 [Install Rust]{.fw-light} + Follow the [rustup installation instructions](https://www.rust-lang.org/tools/install) to install Rust. Note -that the current minimum supported Rust version (msrv) in **extendr** is `1.64`. -This is to ensure CRAN compliance. Windows users will also need to install the GNU toolchain as it matches Rtools. This can be done via rustup in the terminal: `rustup target add x86_64-pc-windows-gnu`. +that the current minimum supported Rust version (msrv) in **extendr** is +`{{< var version.rust >}}`. This is to ensure CRAN compliance. Windows users +will also need to install the GNU toolchain as it matches Rtools. This can be +done via rustup in the terminal: `rustup target add x86_64-pc-windows-gnu`. +::: +:::: +:::::: +::::::: + +::::::: {.card .mt-3 .border-0 .bg-transparent} +:::::: {.row .g-0 .d-flex .flex-column .flex-md-row} +::: {.col-md-4 .d-flex .align-items-center .justify-content-center .p-3 .card-img-col} +[![](images/Rlogo.svg){width="96"}](https://cran.r-project.org/) ::: -## Step 2 [Update R]{.fw-light} +:::: col-md-8 +::: card-body +### Step 2 [Update R]{.fw-light} -::: clearfix -::: {.float-start style="width: 126px; margin-right: 0.8rem;"} -[![](images/Rlogo.svg)](https://cran.r-project.org/) +We recommend using a moderately new version of R (\>= {{< var version.r >}}). +You can get that from [CRAN](https://cran.r-project.org/), but we highly +recommend that you use [rig](https://github.com/r-lib/rig) for this. With rig, +installing R is as simple as `rig add release`. ::: +:::: +:::::: +::::::: -We recommend using a moderately new version of R (\>= 4.2.0). You can get that -from [CRAN](https://cran.r-project.org/). +::::::: {.card .mt-3} +:::::: {.row .g-0 .d-flex .flex-column .flex-md-row} +::: {.col-md-4 .d-flex .align-items-center .justify-content-center .p-3 .card-img-col} +[![](images/rextendr-logo.png){width="96"}](https://extendr.github.io/rextendr/) ::: -## Step 3 [Install rextendr]{.fw-light} +:::: col-md-8 +::: card-body +### Step 3 [Install rextendr]{.fw-light} -::: clearfix -::: {.float-start style="width: 126px; margin-right: 0.8rem;"} -[![](images/rextendr-logo.png)](https://extendr.github.io/rextendr/) +The R package `{rextendr}` provides scaffolding for extendr projects and tools +for documenting Rust functions and objects. To install the latest release +version, use `install.packages("rextendr")`. For the latest development version, +use `pak::pak("extendr/rextendr")`. You can then run `rextendr::rust_sitrep()` +to check your Rust installation. ::: +:::: +:::::: +::::::: -The R package `{rextendr}` provides scaffolding for extendr projects and tools -for documenting Rust functions and objects. To install the latest development -version, use `pak::pak("extendr/rextendr")`. Note that **rextendr** provides a -function to check your Rust installation: `rextendr::rust_sitrep()`. +::::::: {.card .mt-3 .border-0 .bg-transparent} +:::::: {.row .g-0 .d-flex .flex-column .flex-md-row} +::: {.col-md-4 .d-flex .align-items-center .justify-content-center .p-3 .card-img-col} +[![](images/ra-logo-square.svg){width="96"}](https://rust-analyzer.github.io/) ::: -## Step 4 [Use rust-analyzer]{.fw-light} +:::: col-md-8 +::: card-body +### Step 4 [Use rust-analyzer]{.fw-light} This is optional, but strongly recommended. If you use [Visual Studio Code (VS Code)](https://code.visualstudio.com/download) or a similar IDE, the [rust-analyzer](https://rust-analyzer.github.io/) will provide you type hinting and auto-completion suggestions. It is **very** helpful! +::: +:::: +:::::: +::::::: \ No newline at end of file diff --git a/images/extendr-gears-logo.png b/images/extendr-gears-logo.png new file mode 100644 index 0000000..3998c1b Binary files /dev/null and b/images/extendr-gears-logo.png differ diff --git a/images/extendr-gears-logo.svg b/images/extendr-gears-logo.svg new file mode 100644 index 0000000..9a748f6 --- /dev/null +++ b/images/extendr-gears-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/is-odd.png b/images/is-odd.png new file mode 100644 index 0000000..3785192 Binary files /dev/null and b/images/is-odd.png differ diff --git a/images/ra-logo-square.svg b/images/ra-logo-square.svg new file mode 100644 index 0000000..fe1c1fa --- /dev/null +++ b/images/ra-logo-square.svg @@ -0,0 +1,88 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/index.qmd b/index.qmd index a5e3ec2..866231e 100644 --- a/index.qmd +++ b/index.qmd @@ -1,140 +1,55 @@ --- title: "" +toc: false +page-layout: custom +css: css/index.css +anchor-sections: false --- -::: {style="margin:0; width: 100%;"} -::: {style="margin:0; width: 81%; float: left;"} +::::::::: {.hero-banner} -# extendr - extending R with Rust 🦀 +:::::: {#hero-header} +# Build blazingly fast R packages with Rust -[![Github Actions Build -Status](https://github.com/extendr/extendr/workflows/Tests/badge.svg)](https://github.com/extendr/extendr/actions) +Use tools that put developers first. +Get support for publishing to CRAN. + +::: {.mt-3} +[![Github Actions Build Status](https://github.com/extendr/extendr/workflows/Tests/badge.svg)](https://github.com/extendr/extendr/actions) [![Crates.io](https://img.shields.io/crates/v/extendr-api.svg)](https://crates.io/crates/extendr-api) [![Documentation](https://docs.rs/extendr-api/badge.svg)](https://docs.rs/extendr-api) -[![License: -MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - - -::: - -::: {style="margin: 0.25rem 0 0 0; width: 19%; float: right;"} -![](images/extendr-logo-256.png){width="100%" fig-alt="rextendr logo"} +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ::: +:::::: -::: - - - -Build blazingly fast R packages 📦 with ease. The extendr ecosystem is **developer-friendly** and **CRAN**-ready. extendr let's you write ergonomic and idomatic Rust without having to worry about R's internals (much 😅). - -## Getting Started - -- Don't have Rust installed yet? [Start here](/get-started.qmd). -- Familiar with Rust and want to get your hands dirty? Follow along with a [complete example](user-guide/complete-example.qmd). - - -## The ecosystem - -**Rust crates** 🦀 : - -- [`extendr-api`](https://extendr.github.io/extendr/extendr_api)—ergonomic, opinionated, and safe interface between R and Rust -- [`extendr-engine`](https://extendr.github.io/extendr/extendr_engine)—embed and use R in Rust -- [`extendr-ffi`](https://extendr.github.io/extendr/extendr_ffi)—hand-crafted bindings to R's C-API - -**R packages** 📦 : - -- [`{rextendr}`](https://extendr.github.io/rextendr/dev)—A `{usethis}`-like package that scaffolds extendr-powered R packages +:::::: {#hero-cli} +::: {.panel-tabset} +## init +**Get Started** {{< iconify fluent-color:checkmark-circle-32 >}} +Build your first Rust-powered R package in minutes. Follow our step-by-step +guide or dive straight into a complete example. -## Quickstart +→ [Installation](/get-started.qmd) +→ [User Guide](/user-guide/index.qmd) -We recommend using the development version of `{rextendr}` from GitHub. +## run example -```r -# Install from CRAN -install.packages("rextendr") +**See What's Possible** {{< iconify fluent-color:lightbulb-filament-32 >}} +Explore R packages built with extendr. From high-performance data processing to +system integrations, see what the community has built. -# Development version -remotes::install_github("extendr/rextendr") +→ [Browse awesome-extendr](https://github.com/extendr/awesome-extendr) -# create a new R package -usethis::create_package("helloextendr") +## --help -# Use extendr -rextendr::use_extendr() +**Join the Community** {{< iconify fluent-color:people-interwoven-32 >}} +extendr has a welcoming and active community of R and Rust developers. Come ask +questions, share your work, or just hang out. -# Document and build the package -rextendr::document() - -# run hello_world() -hello_world() -#> [1] "Hello, world!" -``` - -### Example: calling structs from R - -``` rust -use extendr_api::prelude::*; - -#[extendr] -#[derive(Debug)] -struct Person { - pub name: String, -} - -#[extendr] -impl Person { - fn new() -> Self { - Self { name: "".to_string() } - } - - fn set_name(&mut self, name: &str) { - self.name = name.to_string(); - } - - fn name(&self) -> &str { - self.name.as_str() - } -} - -#[extendr] -fn my_function() { } - -// Macro to generate exports -extendr_module! { - mod classes; - impl Person; - fn my_function; -} -``` - -The `#[extendr]` attribute causes the compiler to generate wrapper and -registration functions for R which are called when the package is loaded, thus -allowing one to access Rust functions and structures in an R session: - -``` r -# call function -my_function() - -# create Person object -p <- Person$new() -p$set_name("foo") -p$name() # "foo" is returned -``` - -This, of course, is just the tip of the iceberg, for there are many ways to use -extendr in R: - -- In an interactive R session one may use [`rextendr::rust_function` and - friends](https://extendr.github.io/rextendr/reference/rust_source.html) to - quickly prototype Rust code. - -- In an R package context, one may use - [`rextendr::use_extendr()`](https://extendr.github.io/rextendr/reference/use_extendr.html) - to setup a Rust powered R-package. See also the [vignette on - R-packages](https://extendr.github.io/rextendr/articles/package.html). - -- It is also possible to inline Rust code in `Quarto` documents, see - [vignette on extendr - `knitr-engine`](https://extendr.github.io/rextendr/articles/rmarkdown.html). +→ [Discord](https://discord.gg/7hmApuc) +→ [GitHub](https://github.com/extendr/extendr) +::: +:::::: +::::::::: \ No newline at end of file diff --git a/intro-rust/collections.qmd b/intro-rust/collections.qmd index afd4285..c39a98a 100644 --- a/intro-rust/collections.qmd +++ b/intro-rust/collections.qmd @@ -1,101 +1,63 @@ -# Arrays and Vectors - -::: callout-tip - -## Objective - -Learn how to store multiple values of the same type in arrays and vectors. Understand the difference between arrays and vectors. -::: - - -## Arrays - -Arrays in Rust are fixed in size and hold values of the same type. Since the size is known ahead of time, it makes them fast but inflexible. +--- +title: "Collections" +engine: knitr +--- + +Rust has two primary ways to store multiple values of the same type: arrays and +vectors. Arrays are fixed in size — their length is known at compile time, which +makes them fast but inflexible. You cannot add or remove elements after +creation. Vectors are like growable arrays, making them more flexible and also +more common in everyday Rust code. The syntax for creating arrays and vectors +also differs: ```rust -fn main() { - let arr = [10, 20, 30, 40]; - println!("Array: {:?}", arr); -} -``` +// array +let a = [10, 20, 30, 40]; -::: callout-note -The `{:?}` syntax is used for a [**Debug**](https://doc.rust-lang.org/std/fmt/trait.Debug.html) representation of a variable. Using `{}` is used for [**Displaying**](https://doc.rust-lang.org/std/fmt/trait.Display.html) data. - -More often than not, using `{:?}` will be your best option. -::: - -* Arrays use square brackets: `[1, 2, 3]` -* Their size is known at compile time. -* You can't add or remove elements. -* Mostly used when performance is critical and size is known. - -The type of an array is specified as `[type; length]`, e.g. - -```rust -let arr: [i32; 4] = [10, 20, 30, 40]; +// vector +let v = vec![10, 20, 30, 40]; ``` -## Vectors - -Vectors are like growable arrays. They are much more common in everyday Rust code. To create a vector with known values, use the `vec![]` macro. Like an array, they must all be the same type. +Here, the array is created using square brackets, the vector by calling the +`vec!` macro. In this case, the types are inferred. If we want to make those +explicit, the syntax also differs: ```rust -fn main() { - let v = vec![1, 2, 3, 4, 5]; - println!("Vector: {:?}", v); -} -``` - -The type of a vector is specified using `Vec` where `T` is shorthand for any type. - -An empty vector can be created using `Vec::new()` or `vec![]`. If creating an empty vector, the type must be inferred or made explicit. Vectors also have **methods**. Two handy ones are `.len()` for length, and `.is_empty()` (equivalent of `.len() == 0`) +// array +let a: [i32; 4] = [10, 20, 30, 40]; - - -::: callout-important -## Cannot compile -```rust -fn main() { - let x = Vec::new(); - println!("x is empty: {}", x.is_empty()); -} +// vector +let v: Vec = vec![10, 20, 30, 40]; ``` -::: -This cannot compile because the type of `x` is not known. Rust can infer the type if the vector is used elsewhere where the type is known. To make it compile we must specify the type. - -```rust -fn main() { - let x: Vec = Vec::new(); - println!("x is empty: {}", x.is_empty()); -} -```` +In this example, the array type and length are both declared with +`[type; length]`, but the vector only requires specifying that it is a vector of +32-bit integers or `Vec`, which can have any given length. +One nice thing about both vectors and arrays is that you can initialize them to +a specific constant value and size. The syntax here is similar, though again the +vector version requires the use of the `vec!` macro: -## Exercise +```rust +// array +let a: [i32; 4] = [0; 4]; -- Create an array of 4 integers and print it. -- Create a vector with 5 numbers and print it using `{:?}`. -- Compare the length of the array and the vector. -- Bonus: create an empty i32 vector. +// vector +let v: Vec = vec![0; 4]; +``` -### Solution +In each case, we put the initial value `0` followed by the size `4`. -
-View solution +One last thing to note here, specifically about vectors. An empty vector can +also be created with `Vec::new()` or `vec![]`. When creating an empty vector, +however, Rust needs to know the element type either from context or from an +explicit type annotation, since it cannot infer type from the empty vector +itself: ```rust -fn main() { - let arr = [1, 2, 3, 4]; - println!("Array: {:?}", arr); - - let v = vec![10, 20, 30, 40, 50]; - println!("Vector: {:?}", v); - - // Bonus: - let v = vec![42_i32; 0]; -} +let v: Vec = Vec::new(); ``` -
+Vectors also have a number of useful **methods**: `.len()` returns the number of +elements and `.is_empty()` returns `true` if there are no elements. We will +encounter more methods like these as we work through the guide. diff --git a/intro-rust/control-flow.qmd b/intro-rust/control-flow.qmd new file mode 100644 index 0000000..b20c833 --- /dev/null +++ b/intro-rust/control-flow.qmd @@ -0,0 +1,87 @@ +--- +title: "Control Flow" +engine: knitr +--- + +Control flow refers to the ability to conditionally execute code. As with R, +Rust declares conditions for code execution using `if`, `else` and `else if`. +Those conditions will typically involve logical operators: + +- `==` check equality +- `!=` check inequality +- `!` negate a logical value +- `&&` logical AND comparison +- `||` logical OR comparison + +Logical operators work similarly to R but are not vectorised. They return a +single logical called a `bool` instead and can only take on one of two values: +`true` or `false`. + +Like R, each branch of an `if` in Rust must be delimited by curly brackets, but +unlike R, parentheses around the condition are not required: + +```rust +if x == y { + // do something +} else { + // do something else +} +``` + +Note, too, that the result of control flow can be assigned to a variable: + +```rust +let number = if x == y { 5 } else { 3 }; +``` + +Given what we learned about Rust's strong typing system, this suggests an +additional strong constraint on control flow: each branch or "arm" of an `if` +expression must return the same type. Otherwise, the type would be unknown at +compile time. Consider this example program (drawn straight from The Book) that +includes mismatched types in the `if` and `else` arm:. + +```{.rust filename="src/main.rs"} +fn main() { + let condition = true; + + let number = if condition { 5 } else { "six" }; + + println!("The value of number is: {number}"); +} +``` + +If you try to compile and run this program, you will get the following error +message from cargo: + +```default +error[E0308]: `if` and `else` have incompatible types + --> src/main.rs:4:44 + | +4 | let number = if condition { 5 } else { "six" }; + | - ^^^^^ expected integer, found `&str` + | | + | expected because of this + +For more information about this error, try `rustc --explain E0308`. +``` + +As an side, this is actually a great example of one of Rust's best features. Not +only does the error message tell you where precisely it occurs, but it also +points you to additional information to help you understand what is wrong in +your code. Often, it will even suggest how to fix your problem! + +For completeness, here is the correct way to program that control flow: + +```{.rust filename="src/main.rs"} +fn main() { + let condition = true; + + let number = if condition { 5 } else { 6 }; + + println!("The value of number is: {number}"); +} +``` + +```default +The value of number is: 5 +``` \ No newline at end of file diff --git a/intro-rust/enumerations.qmd b/intro-rust/enumerations.qmd new file mode 100644 index 0000000..78adf89 --- /dev/null +++ b/intro-rust/enumerations.qmd @@ -0,0 +1,199 @@ +--- +title: "Enumerations" +engine: knitr +--- + +Sometimes a value can only take on one of a fixed set of possibilities. In R, +this pattern appears constantly as function arguments. For example, the `cor()` +function has the argument `method = c("pearson", "kendall", "spearman")`. Rust +formalizes this idea with *enumerations* or`enum`s, types that can take on +exactly one of a defined set of *variants*. + +::: {.callout-note} +The tidyverse design style guide has a great section on enums. See +[enumerate options](https://design.tidyverse.org/enumerate-options.html#whats-the-pattern). + +Josiah Parry's blog also has a nice discussion of this. See +[Enums in R: towards type safe R](https://josiahparry.com/posts/2023-11-10-enums-in-r/). +::: + +Like structs, enum names use PascalCase, and variants are created using +`EnumName::Variant`: + +```rust +enum Shape { + Triangle, + Rectangle, + Pentagon, + Hexagon, +} + +let my_shape = Shape::Triangle; +``` + +## Matching + +How do we actually determine behavior based on a variant? This is done using +*pattern matching*, specifically the keyword `match`, which works similar to +`switch()` in R. The pattern match format uses the syntax `Enum::Variant => action`. +When using `match` each variant much be *enumerated*: + +```{.rust filename="src/main.rs"} +enum Shape { + Triangle, + Rectangle, + Pentagon, + Hexagon, +} + +fn main() { + let my_shape = Shape::Triangle; + + match my_shape { + Shape::Triangle => println!("A triangle has 3 vertices"), + Shape::Rectangle => println!("A rectangle has 4 vertices"), + Shape::Pentagon => println!("A pentagon has 5 vertices"), + Shape::Hexagon => println!("A hexagon has 6 vertices"), + } +} +``` + +```default +A triangle has 3 vertices +``` + +When you only care about specific variants, use `_` as a catch-all for +everything else: + +```{.rust filename="src/main.rs"} +enum Shape { + Triangle, + Rectangle, + Pentagon, + Hexagon, +} + +fn main() { + let my_shape = Shape::Triangle; + + match my_shape { + Shape::Hexagon => println!("Hexagons are the bestagons"), + _ => println!("Every other polygon is mid"), + } +} +``` + +```default +Every other polygon is mid +``` + +## Methods + +Enums can have `impl` blocks just like structs. Inside the method, `match self` +branches on the variant: + +```{.rust filename="src/main.rs"} +enum Shape { + Triangle, + Rectangle, + Pentagon, + Hexagon, +} + +impl Shape { + fn n_vertices(&self) -> i32 { + match self { + Self::Triangle => 3, + Self::Rectangle => 4, + Self::Pentagon => 5, + Self::Hexagon => 6, + } + } +} + +fn main() { + let my_shape = Shape::Triangle; + let n_vertices = my_shape.n_vertices(); + + println!("my shape has {} vertices", n_vertices); +} +``` + +```default +my shape has 3 vertices +``` + +## Missing values + +Because of its memory model and strong safety guarantees, Rust has no concept of +a `NULL` type. Null values are actually quite dangerous and have their own +category of errors and security issues. In fact, [C.A.R. Hoare](https://en.wikipedia.org/wiki/Tony_Hoare), +who invented null pointers, called them his "billion-dollar mistake." Rust, +therefore, makes it impossible to use null values, requiring instead that the +concept of missingness be implemented using a special kind of `enum` known as an +`Option<>`, which has exactly two variants: + +```rust +enum Option { + Some(T), + None, +} +``` + +As a reminder, the `T` here stands for any generic type. When a value is +present, it is wrapped in `Some(T)`. When it is absent, the variant is `None`. +The type system forces you to acknowledge the possibility of `None` before you +can use the inner value — there is no way to accidentally treat a missing value +as if it were present. + +Since `Option` is just an enum, we can match on the variants to access its +values. + +```rust +// create an Option that contains a value +let measure = Some(Measure::Euclidean); + +match measure { + Some(v) => println!("The measure is: {}", v), + None => println!("Oh no! The measure is missing!"), +} +``` + +This makes missing values visible - the code cannot compile unless both arms are +handled. + +::: {.callout-warning} +Sometimes dealing with options is a headache, particularly when we're in the +early stages of developing. While we can use `.unwrap()` or `.expect()` to grab +the inner value of an option _without matching_, this is **dangerous!!** because +we are ignoring the possibility of a `None`. Unwrapping on a `None` leads to a +**panic**. Panics cause your program to abort. + +```rust +thread 'main' panicked at src/main.rs:4:41: +called `Option::unwrap()` on a `None` value +``` +::: + +## Optional arguments + +A common use is to make function arguments optional — call with a value when you +have one, and fall back to a default when you do not, similar to how we use +`function(x = NULL)` in R. This can be achieved using an option in Rust. + +```{.rust filename="src/main.rs"} +fn greet_user(name: Option) { + let user_name = name.unwrap_or(String::from("world")); + println!("Hello, {}!", user_name); +} + +fn main() { + greet_user(None); // Output: Hello, world! + greet_user(Some("Ferris".to_string())); +} +``` + +```default +Hello, world! +Hello, Ferris! +``` diff --git a/intro-rust/enums.qmd b/intro-rust/enums.qmd deleted file mode 100644 index 6120f8c..0000000 --- a/intro-rust/enums.qmd +++ /dev/null @@ -1,192 +0,0 @@ -# Enum(eration)s - -::: callout-tip -## Objective - -Understand what `enum`s are and when you may want to use one. - -::: - -Oftentimes a variable can only take on a small number of values. This is when an **enumeration** (enum) would be useful. - -`enum`s are values that can only take on a select few **variants**. They are defined using the `enum` keyword. - - - -## Enums in R - -We use enums _all the time_ in R and almost exclusively as function arguments. - -::: aside -The tidyverse design style guide has a great section on enums. See [enumerate options](https://design.tidyverse.org/enumerate-options.html#whats-the-pattern). -::: - -```r -args(cor) -``` -```r -function( - x, y = NULL, use = "everything", - method = c("pearson", "kendall", "spearman") # 👈🏼 enum! -) -``` - -::: aside -I've written about this in more detail in my blog post [Enums in R: towards type safe R](https://josiahparry.com/posts/2023-11-10-enums-in-r/) -::: - -### Example - -For example, it may make sense to create an `enum` that specifies a possible shape. - -```rust -enum Shape { - Triangle, - Rectangle, - Pentagon, - Hexagon, -} -``` - -## Variant-specific behavior - -The `Shape` enum can take on only one of those 4 **variants**. An enum is created using the format `EnumName::Variant`. - -How do we actually determine behavior based on a variant? This is done using **pattern matching**. - -The keyword `match` lets us perform an action based on an enum's variant. It works by listing each variant and the behavior to that happens when that variant is matched. - -The pattern match format uses the syntax `Enum::Variant => action`. When using `match` each variant much be _enumerated_: - -::: aside -`=>` is often referred to as "fat arrow." But if you say "chompky" I'll also get it. -::: - -```rust -match my_shape { - Shape::Triangle => todo!(), - Shape::Rectangle => todo!(), - Shape::Pentagon => todo!(), - Shape::Hexagon => todo!(), -} -``` - -::: callout-tip -todo!() is a placeholder that can be used to make the compiler happy -::: - - -### Example - -For example we may want to print the number of verticies of a shape: - -```rust -let my_shape = Shape::Triangle; - -match my_shape { - Shape::Triangle => println!("A triangle has 3 vertices"), - Shape::Rectangle => println!("A rectangle has 4 vertices"), - Shape::Pentagon => println!("A pentagon has 5 vertices"), - Shape::Hexagon => println!("A hexagon has 6 vertices"), -} -``` - -## Wildcard matching - -Sometimes we want to customize behavior on only a subset of variants. We can use a catch all in the match statement `_ =>` use the underscore to signify "everything else". - -```rust -match my_shape { - Shape::Hexagon => println!("Hexagons are the bestagons"), - _ => println!("Every other polygon is mid"), -} -``` - -## Enums can `impl`, too - -Enums can have methods too just like a struct using `impl` keyword - -```rust -impl Shape { - fn is_bestagon(&self) -> bool { - match self { - Self::Hexagon => true, - _ => false - } - } -} -``` - - -## Exercise 1 - -- Create an enum called `Measure` with two variants `Euclidean` and `Haversine` -- Create a method called `ndim()` which returns `2` for `Euclidean` and `3` for `Haversine` - -
- View solution - -```rust -enum Measure { - Euclidean, - Haversine, -} - -impl Measure { - fn ndim(&self) -> i32 { - match self { - Self::Euclidean => 2, - Self::Haversine => 3 - } - } -} -``` - -
- -## Exercise 2 - - -- Create a new method `distance()` for `Point` struct that returns an `f64` - - Arguments: `&self`, `destination: &Self`, `measure: &Measure` -- When `measure` is `Euclidean` use the `euclidean_distance()` method -- When the variant is `Haversine` use the `haversine_distance()` method - -The haversine method is defined as: - -
-Code for `haversine_distance()` -```rust -impl Point { - fn haversine_distance(&self, destination: &Self) -> f64 { - let radius = 6_371_008.7714; // Earth's mean radius in meters - let theta1 = self.y.to_radians(); // Latitude of point 1 - let theta2 = destination.y.to_radians(); // Latitude of point 2 - let delta_theta = (destination.y - self.y).to_radians(); // Delta Latitude - let delta_lambda = (destination.x - self.x).to_radians(); // Delta Longitude - - let a = (delta_theta / 2f64).sin().powi(2) - + theta1.cos() * theta2.cos() * (delta_lambda / 2f64).sin().powi(2); - - 2f64 * a.sqrt().asin() * radius - } -} -``` - -
- - -
-View solution -```rust - -impl Point { - // Demonstrates using pattern matching an enum - fn distance(&self, destination: &Self, measure: &Measure) -> f64 { - match measure { - Measure::Euclidean => self.euclidean_distance(destination), - Measure::Haversine => self.haversine_distance(destination), - } - } -} -``` diff --git a/intro-rust/fizz-buzz.qmd b/intro-rust/fizz-buzz.qmd deleted file mode 100644 index 6ac7531..0000000 --- a/intro-rust/fizz-buzz.qmd +++ /dev/null @@ -1,78 +0,0 @@ -# Control Flow - -::: callout-tip -## Objective - -Understand control flow and numeric operators in Rust. You will create the `FizzBuzz` program using Rust! - -::: - -## Numeric operators - -- `+` addition -- `-` subtraction -- `/` division -- `%` remainder - -## Logical Operators: - -Logical operators are quite similar to R. The difference is that these operations are not vectorised. Furthermore, in Rust, a logical is called `bool` for booleans. `bool`s can take on only two values: `true` or `false`. - -- `==` check equality -- `!=` check inequality -- `!` negate a logical value -- `&&` logical AND comparison -- `||` logical OR comparison - - -## Control flow - -Rust uses `if`, `else`, and `else if` statements just like R. Where each branch is delimted by curly braces. - -::: callout-warning -Each branch of the `if` statement must return the same type. For this portion of the workshop, be sure to **terminate** each statement with `;` inside of the `if` statement so that nothing (unit type) is returned. -::: - - -```rust -if x == y { - // do something -} else { - // do something else -} -``` - -The key difference is that the use of parentheses is not necessary for the conditional statement. - - -## Exercise - -This exercise you will create the famous `FizzBuzz` program. - -For this, create a variable `i`. The rules are: - -- when `i` is a multiple of `3`, print `Fizz` -- when `i` is a multiple of `5`, print `Buzz` -- when `i` is a multiple of _both_ `3` and `5`, print `FizzBuzz` - - -### Solution - -
-View solution -```rust -fn main() { - // let i = 15; // FizzBuzz - // let i = 3; // Fizz - // let i = 5; // Buzz - let i = 47; // Nothing - if (i % 3 == 0) && (i % 5 == 0) { - println!("FizzBuzz"); - } else if i % 3 == 0 { - println!("Fizz"); - } else if i % 5 == 0 { - println!("Buzz"); - } -} -``` -
diff --git a/intro-rust/for-loops.qmd b/intro-rust/for-loops.qmd deleted file mode 100644 index 48c843f..0000000 --- a/intro-rust/for-loops.qmd +++ /dev/null @@ -1,128 +0,0 @@ -# `for`-loops - -::: callout-tip - -## Objective - -Be able to iterate over a collection of values. Apply logic inside of the for loop. - -::: - -## `for` loop syntax - -In Rust, `for` loops are the easiest way to go over each item in a vector or array. - -```rust -for value in collection { - // do something with value -} -``` - -When we iterate through a collection, each item takes on the value of the name you provide before `in`. For example `for x in my_vec` creates a binding to `x` at each iteration. - -```rust -fn main() { - let nums = vec![1, 2, 3]; - for n in nums { - println!("n is: {}", n); - } -} -``` - - -## Scope - -R has some of the most flexible and unique scoping rules. Rust isn't as kind. The rules are essentially: - -* Values *outside* of the for loop are accessible inside of it. -* Values created *inside* of the for loop cannot be accessed outside of it. - - -### Example: Outer value used inside loop - -:::{.callout-tip title = "This does compile" icon=false} -```rust -fn main() { - let greeting = "Hi"; - let names = vec!["Alice", "Bob"]; - - for name in names { - println!("{greeting}, {name}!"); - } -} -``` -::: - -### Example: Inner value not usable outside loop - -::: callout-important -## This does not compile! - - -```rust -fn main() { - let numbers = vec![1, 2, 3]; - - for n in numbers { - let doubled = n * 2; - println!("{n} doubled is {doubled}"); - } - - // ❌ `doubled` doesn't exist here - // println!("Last doubled: {}", doubled); -} -``` -::: - - -## Exercise - -Using a vector of integers, write a loop that prints: - -* "Fizz" if divisible by 3 -* "Buzz" if divisible by 5 -* "FizzBuzz" if divisible by both -* The number otherwise - -Use this vector: `vec![1, 2, 3, 4, 5, 15]` - -### Solution - -
-View hint - -```rust -fn main() { - let nums = vec![1, 2, 3, 4, 5, 15]; - - for n in nums { - // add fizz-buzz logic here referring to `n` - } -} -``` - -
- - -
-View solution - -```rust -fn main() { - let nums = vec![1, 2, 3, 4, 5, 15]; - - for n in nums { - if n % 15 == 0 { - println!("FizzBuzz"); - } else if n % 3 == 0 { - println!("Fizz"); - } else if n % 5 == 0 { - println!("Buzz"); - } else { - println!("{}", n); - } - } -} -``` - -
diff --git a/intro-rust/functions.qmd b/intro-rust/functions.qmd new file mode 100644 index 0000000..26b4665 --- /dev/null +++ b/intro-rust/functions.qmd @@ -0,0 +1,44 @@ +--- +title: "Functions" +engine: knitr +--- + +So far, we have been modifying the `main()` function, but Rust lets us define as +many functions as we need. In R, functions are objects created with +`<- function()`. In Rust, they are declared with the `fn` keyword. Just like +everything else in Rust, arguments are typed - each argument takes the form +`arg_name: type`, and the return type is specified after `->`. + +```rust +fn name_of_function(arg1: ArgType) -> ReturnType { + // function body + my_return_object +} +``` + +As in R, the last expression in a function body is returned automatically — no +explicit `return` keyword is needed unless you are returning early. Functions +can also be declared anywhere in the file, outside of `main()`. + +Identifying whether a number is odd or even is a classic example. The +[is-odd npm package](https://www.npmjs.com/package/is-odd) became famous for +being a remarkably small and widely depended-upon piece of code: + +![](/images/is-odd.png){width=70% fig-align="center"} + +Here is the Rust equivalent. We define `is_even()` first, taking an `i32` and +returning a `bool`, then use it inside `is_odd()`: + +```rust +fn is_even(x: i32) -> bool { + x % 2 == 0 +} + +fn is_odd(x: i32) -> bool { + !is_even(x) +} +``` + +Since `x % 2 == 0` is an expression that evaluates to a `bool`, it is returned +directly without a `return` statement. Building new functions from existing ones +like this is idiomatic Rust. diff --git a/intro-rust/hello-world.qmd b/intro-rust/hello-world.qmd index cd43500..a43e748 100644 --- a/intro-rust/hello-world.qmd +++ b/intro-rust/hello-world.qmd @@ -1,128 +1,108 @@ -# Hello, World! - - -::: callout-tip -## Objective - -Learn how to create a new Rust crate and understand its file structure. You will know how to use `cargo run` and the `println!()` macro. -::: - - -Rust uses a tool called `cargo` for building, checking, and managing dependencies. This is installed for you when you used `rustup` to install Rust. - -To create a new Rust crate, use `cargo new name-of-crate`. This creates a new directory called `name-of-crate`. Be sure to `cd` into that directory. - -::: aside -`cd` is short for change directory. We use this to...yup...change the directory from the terminal. -::: - -## Crate anatomy - -Two types of crates: binary, library. - -::: aside -This first workshop we will work only with a binary crate. We will create a library in the second half of the day. -::: - -Binary crates are standalone applications like command line tools, or things that run once—simiar to a script that you run with `Rscript main.R` - - -## Crate anatomy - -A new crate looks like this: - -``` -intro-to-rust/ -├── Cargo.toml # Metadata & dependencies (like DESCRIPTION) -├── Cargo.lock # Dependency versions (like renv.lock) +--- +title: "Hello, World!" +engine: knitr +--- + +In this tutorial, you will learn how to setup a new Rust crate and get a sense +for what it includes. You will also learn how to build and run your new crate +with `cargo`, a package manager and build system for Rust. You do not need to +install `cargo` because it comes bundled with Rust. + +When you first setup a Rust crate, you will have to choose between one of two +types: binary crates and library crates. Binary crates are standalone +applications or executables that you can run, like command line tools. Library +crates are typically small bundles of code that are used in the development of +other crates. In this tutorial, we will focus on building a binary crate. + +To create a new binary crate, open a terminal and run the following: + +```default +$ cargo new hello-world +``` + +This will create a new directory called `hello-world` that you can then navigate +into using `cd hello-world`. Once inside that directory, you will find all of +the following: + +``` +hello-world/ +├── Cargo.toml +├── Cargo.lock └── src/ - └── main.rs # Entry point — like main.R + └── main.rs ``` +The `Cargo.toml` file is a document in which you specify metadata and +dependencies for your crate similar to an R package's `DESCRIPTION`. The +`Cargo.lock` file maintains copies of those dependencies, similar to +`renv.lock`. A lot more can be said about those two files, which are absolutely +crucial to programming in Rust, but for now let's focus on the Rust file in the +source directory. If you open that file in Positron (or your preferred IDE), you +will see the following text: -## `main.rs` - -When you create a new rust binary the file `src/main.rs` is prepopulated with: - -::: callout-tip -## `src/main.rs` - -The `main()` function defines what is executed when your _binary_ is run. - - -```rust +```{.rust filename="src/main.rs"} fn main() { println!("Hello, world!"); } ``` -::: +In just those three short lines, we see examples of many Rust coding patterns: -There are a few things going on in here: +- Functions are declared using the `fn` keyword, +- The `main()` function defines what happens when you run your binary, +- Blocks of code are delimited using curly brackets (just like R), and +- Statements end with `;`. -- Functions are declared using the `fn` keyword -- The `main()` function is the entrypoint of the program (and required) -- Blocks of code are delimted using curly braces (like R & C) -- Statements end with `;` -- `println!()` is a macro (notice the `!`) which is used to print to `stdout` +Right now, our app uses the `println!()` macro to print "Hello, world!" to the +terminal. When a program writes to the console, it does so through file +connections called standard output (`stdout`) and standard error (`stderr`). In +R, when we print a message with `print()` or `message()`, we print to stdout. +Similarly, when we make a warning or error using `stop()` or `warning()`, that +is writing to `stderr`. The Rust `println!()` macro also has the nice feature of +supporting string interpolation like `sprintf()` and `glue()`: -::: aside -When a program writes to the console it does so through file connections called standard output (`stdout`) and standard error (`stderr`). - -When we print a message with `print()` or `message()` in R, we print to stdout. When we make a warning or error using `stop()` or `warning()` in R, that is writing to `stderr`. -::: - - -::: callout-important - -Variables can only be defined inside of the `main()` function. - -::: - -## `println!()` - -* Macros have a `!`, like `println!()`. -* Think of it like `print()` in R, but explicit. -* It supports format strings: - - -:::{layout-ncol=2} -#### Using placeholder -```rust +``` rust +// indirect interpolation let name = "Josiah"; println!("Hello, {}!", name); -``` -#### Direct interpolation -```rust +// direct interpolation let name = "Josiah"; println!("Hello, {name}!"); ``` -::: - +If we add one or the other of those lines to `main()` like so -## Exercise - -- In your terminal, create a new rust crate called `intro-to-rust` -- Open the new Rust crate's folder in Positron -- Run the hello world program using `cargo run` -- Create a variable called `name` inside of the `main()` function with your name -- Print `Hello, {name}!` using `println!()` -- To run it navigate to your terminal and then enter `cargo run`. - -### Solution - -
-View solution - -In `src/main.rs` - -```rust +```{.rust filename="src/main.rs"} fn main() { let name = "Josiah"; println!("Hello, {name}!"); } ``` -
+we can then call the application and get it to execute in the terminal: + +```default +$ cargo run + Compiling hello_world v0.1.0 (/hello-world) + Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs + Running `target/debug/hello_world` +Hello, Josiah! +``` + +Notice that `cargo` provides detailed information about the build process +(printed to stderr) before actually running the program (and printing to +stdout). This is a useful reminder that Rust, unlike R, is a compiled language - +Rust programs have to be built before they can be run. For those coming from R, +that might sound rather mysterious, and in some respects it is, but we want to +emphasize how easy it was to get a program up and running. It was just three +lines of code in the terminal: + +```default +$ cargo new hello-world +$ cd hello-world +$ cargo run +``` + +That's it! Those three lines were sufficient to build and run our simple binary. +Now we can move on to learning about basic data types in Rust. diff --git a/intro-rust/implementations.qmd b/intro-rust/implementations.qmd new file mode 100644 index 0000000..74acf25 --- /dev/null +++ b/intro-rust/implementations.qmd @@ -0,0 +1,125 @@ +--- +title: "Implementations" +engine: knitr +--- + +Other methods can be implemented for structs using `impl` blocks. An `impl` +block is roughly analogous to defining S3 methods in R, except Rust's `impl` +blocks associate behavior directly with the type definition, making them +object-oriented. The syntax is this: + +```rust +impl TypeName { + fn method_name() { + // body + } +} +``` + +## Associated functions + +An *associated function* is called on the type itself rather than on an +instance. These are written as `TypeName::function()` and are typically used +as constructors. The return type `Self` refers to the type being implemented: + +```{.rust filename="src/main.rs"} +#[derive(Debug)] +struct Person { + name: String, + age: i32 +} + +impl Person { + fn new(name: String, age: i32) -> Self { + Person { name, age } + } +} + +fn main() { + let me = Person::new("Josiah".to_string(), 29); + println!("{me:?}"); +} +``` + +```default +Person { name: "Josiah", age: 29 } +``` + +You have already seen associated functions in this form: `Vec::new()` and +`String::new()` are both associated functions. + +## Self-reference + +To define a method that operates on an instance, add `&self` as the first +argument. This provides a read-only reference to the struct, so you can access +its fields but not move or modify them: + +```rust +impl Person { + fn greet(&self) { + println!( + "Hello, my name is {} and I am {} years old.", + self.name, self.age + ); + } +} +``` + +A method can also accept another instance of the same type by using `&Self` as +an argument type. Here `is_older_than()` compares two `Person` values: + +```rust +impl Person { + fn is_older_than(&self, other_person: &Self) -> bool { + self.age > other_person.age + } +} +``` + +## Mutable self + +To modify a struct's fields from a method, use `&mut self`. Here `rename()` and +`celebrate_birthday()` both mutate fields in place: + +```{.rust filename="src/main.rs"} +#[derive(Debug)] +struct Person { + name: String, + age: i32 +} + +impl Person { + fn new(name: String, age: i32) -> Self { + Person { name, age } + } + + fn greet(&self) { + println!( + "Hello, my name is {} and I am {} years old.", + self.name, self.age + ); + } + + fn rename(&mut self, new_name: &str) { + self.name = new_name.to_string(); + } + + fn celebrate_birthday(&mut self) { + self.age += 1; + } +} + +fn main() { + let mut me = Person::new("Josiah".to_string(), 29); + me.celebrate_birthday(); + me.rename("Jo"); + me.greet(); +} +``` + +```default +Hello, my name is Jo and I am 30 years old. +``` + +Note that the instance must also be declared `mut` when it is assigned to a +variable. \ No newline at end of file diff --git a/intro-rust/index.qmd b/intro-rust/index.qmd index ab28fee..c651308 100644 --- a/intro-rust/index.qmd +++ b/intro-rust/index.qmd @@ -1,35 +1,58 @@ --- -title: Rust for R Developers +title: A Brief Introduction to Rust +page-layout: article +toc: true +toc-depth: 0 +date: today +date-format: long --- -You want to build Rust powered R packages but don't know where to start? +Want to build Rust powered R packages but don't know where to start? This guide +will walk you through the basics of Rust programming using familiar R concepts. -## What is this? +## Why Rust? 🦀 -This is an opinionated and streamlined introduction to Rust to get you building **fast** ⚡️, and **vectorized** R functions within a few hours. +Unlike R and Python, which are *interpreted* languages, Rust is a *compiled* +language. That means it can often times be much, much faster and more efficient, +similar to other compiled languages like C or C++. But where those languages can +be hard to use and easy to break, Rust was built to be safer and more helpful. +Rust is especially good at preventing bugs related to memory. There’s no garbage +collector in Rust, so it is quite memory efficient. Moreover, Rust is designed +with the developer in mind. The Rust compiler provides error messages that rival +— or maybe even surpass — the quality of tidyverse error messaging. -## Who is it for? - -We get it. Learning Rust can be daunting and frankly, much of the "Intro to Rust" material out there isn't immediately applicable to R or data science. - -This introduction is designed for **intermediate R developers**. +## Prerequisites +This introduction is designed for **intermediate R developers** who are familiar +with fundamental computing concepts, including data types (like floats, +integers, and booleans), iteration (with `for` and `while` loops, `purrr::map()` +style iterators, and the `apply()` family of functions), control flow, and +functions. Being comfortable with a terminal environment will be helpful, too. -## Prerequisites +Regardless of whether you meet those prerequisites, you may find it helpful to +freshen up on your R fundamentals by reading or reviewing the following +materials: +- [R for Data Science](https://r4ds.hadley.nz) +- [Hands-On Programming with R](https://rstudio-education.github.io/hopr) +- [Advanced R](https://adv-r.hadley.nz) -It is recommended that you be familiar with fundamental computing concepts such as: +As for Rust, a lot of the materials in this brief introduction come from the +official guide, [The Rust Programming Language](https://doc.rust-lang.org/book/), +what Rust developers affectionately call "The Book." -- Data types such as floats, integers, and booleans -- Iteration such as for / while loops, `purrr::map()` style iterators, and the `apply()` family of functions -- Control Flow -- Writing functions +## Software requirements -### Resources +This guide assumes that you have Rust {{< var version.rust >}} installed. Please +see [Get Started](get-started.qmd) if you do not. -- Read [Advanced R Chapter 3](https://adv-r.hadley.nz/vectors-chap.html) -- Read [Hands-On Programming with R](https://rstudio-education.github.io/hopr/loops.html), [R for Data Science Chapter 26](https://r4ds.hadley.nz/iteration.html) -- Read [Advanced R Chapter 4](https://adv-r.hadley.nz/control-flow.html) -- Read [R for Data Science Chapter 25](https://r4ds.hadley.nz/functions.html), [Advanced R Chapter 6](https://adv-r.hadley.nz/functions.html) +If you are not yet comfortable with installing Rust, you may find it helpful to +try out examples in this tutorial using the open source +[Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021) +browser app. +## Acknowledgment +These introductory tutorials are based on the +[Rust for R Developers](https://josiahparry.github.io/2025-cascadia-rust-for-r-devs/) +workshop led by Josiah Parry at the 2025 Cascadia R Conference. \ No newline at end of file diff --git a/intro-rust/is-odd.qmd b/intro-rust/is-odd.qmd deleted file mode 100644 index db004ef..0000000 --- a/intro-rust/is-odd.qmd +++ /dev/null @@ -1,88 +0,0 @@ -# Functions - -::: callout-tip -Learn how to define and use your own functions in Rust. -::: - -We've already been modifying the `main()` function this whole time. We can create and use additional functions too. - -In R, functions are yet another type of object that we create using `<- function()`. In Rust, they are declared using `fn` keyword. Just like everything else in Rust, arguments are typed. Arguments take the structure of `arg: type`. - -Much like R, the last object in a function body is return automatically. You do not need to use the `return` keyword unless you are performing an early return. - - - -```rust -fn name_of_function(arg1: ArgType) -> ReturnType { - // function body - my_return_object -} -``` - -## Example - -Identifying if a number is odd or even isn't always so easy! - -![is-odd npm](/images/is-odd.png){width=70%} - -First define a function called `is_even()` that takes an `i32` (integer) and returns a `bool`. - -::: aside -It may not be entirely obvious, but functions can be declared outside of the `main()` function. -::: - -```rust -fn is_even(x: i32) -> bool { - x % 2 == 0 -} -``` - -We can use our already defined function inside of another: - -```rust -fn is_odd(x: i32) -> bool { - !is_even(x) -} -``` - - -## Exercise - -Create a function called `mean()` that calculates the mean of a `Vec`. - -* In `main()`, create a vector `x` with 5 or more `f64` values. -* Call `mean(x)` and print the result. - -::: callout-note -Use `x.len()` to get the length and `as f64` to convert it to a float. -::: - -### Solution - -
-View solution - -```rust -fn mean(x: Vec) -> f64 { - let mut total = 0.0; - let n = x.len(); - for xi in x { - total += xi; - } - total / n as f64 -} - -fn main() { - let x = vec![1.0, 2.0, 3.0, 4.0, 5.0]; - let result = mean(x); - println!("Mean is: {}", result); -} -``` - -
diff --git a/intro-rust/iter-map.qmd b/intro-rust/iter-map.qmd deleted file mode 100644 index 52b2e2a..0000000 --- a/intro-rust/iter-map.qmd +++ /dev/null @@ -1,208 +0,0 @@ -# Mapping over Iterators - -::: callout-tip -## Objective -- Understand how to transform iterator values using `.map()` -- Learn how to use closures (anonymous functions) in Rust -- Collect iterator results back into a collection using `.collect()` -- Use turbofish 🐠💨 syntax for type annotations when needed -::: - -Often we want to transform the values in a vector. To do this in Rust, we can use the `.map()` method. `.map()` lets us apply an operation on each element of the iterator. - -In R, we often use the `purrr::map()` function or the `apply()` family of functions to do this. - -## Using `.map()` - -We apply `.map()` to an iterator. For example, to square each element in a vector we can write: - -```rust -// providing explicit type annotation in one -// value so the compiler knows its f64 and not f32 -let x = vec![5.9_f64, 6.8, 4.5, 7.3, 6.2]; - -x.iter().map(|xi| xi.powi(2)) -``` - -Using `.map()` returns another iterator. We can chain operations over iterators by using multiple `.map()` statements. - - -## Closures - -In the above example we used a **closure** to modify each element of the iterator. Closures are Rust's version of an anonymous function. - -A closure takes the structure `|arg| expression`. They can also be multiple lines by wrapping the expression in a braces: - -```rust -x - .iter() - .map(|xi| { - let squared = xi.powi(2); - xi.sqrt() - }) -``` - -Since iterators only contain one item, the closure only has one argument. However, you can use **destructuring** in the closure. For example if using `.enumerate()` we want to access `i` we can do so: - -```rust -x - .iter() - .enumerate() - .map(|(i, xi)| { - // do stuff here - }) -``` - -## Collecting results - -In R when we use `map()` the results are always returned as a vector. In Rust, we have to explcitly **collect** the iterator into our own type using `.collect()`. - -Typically iterators are collected into a `Vec`. Rust cant always infer the type that you want to collect into so we must tell the compiler what type we want. - -We can do this during assignment: - -```rust -let x_squared: Vec = x.iter().map(|xi| xi.powi(2)).collect(); -``` - - -## Turbofish 🐠💨 - -Alternatively, we can use "turbofish" syntax. We can specify the type directly in the `.collect()` function. `.collect()` is a **generic** method. And in the words of the Rust standard library documentation (emphasis mine) - -> "Because `collect()` is so general, it **can cause problems with type inference**. As such, `collect()` is one of the few times you’ll see the syntax affectionately known as the ‘turbofish’: `::<>`. This helps the inference algorithm understand specifically which collection you’re trying to collect into." - -Turbofish takes the structure of `.collect::()`. - - - -### Examples - -:::{layout-ncol="2"} -#### Explicit typing -```rust -fn main() { - let nums = vec![1, 2, 3]; - - // Add 1 to each element - let incremented: Vec<_> = nums.iter() - .map(|x| x + 1) - .collect(); - - println!("{:?}", incremented); -} -``` - -#### Inference w/ turbofish -```rust -fn main() { - let nums = vec![1, 2, 3]; - - // Add 1 to each element - let incremented = nums.iter() - .map(|x| x + 1) - .collect::>(); - - println!("{:?}", incremented); -} -``` -::: - -Turbofish is a bit more awkward at first, but it is more flexible and doesn't require modification whenever the inner type changes. - -## Exercise 1 - -Calculate the **variance** of a slice of `f64` values. - -$$ -\text{variance} = \frac{\sum_{i=1}^n (x_i - \bar{x})^2}{n - 1} -$$ - -* Create a function `variance()` that: - - * Uses `.map()` to calculate squared differences from the mean - * Uses `.sum()` to add them up - * Divides by `n - 1` - -::: callout-tip - -* Use `.powi(2)` to square values. -* Use your previously defined `mean()` function inside `variance()`. -::: - - -
-View hint - -```rust -fn variance(x: &[f64]) -> f64 { - let n = x.len() as f64; - let avg = mean(x); - let sq_diffs: f64 = x - .iter() - .map(|xi| ___ ) // squared difference here - .__(); // sum method here - - sq_diffs / (n - 1.0) -} -``` - -
- -### Solution - -
-View solution - -```rust -fn variance(x: &[f64]) -> f64 { - let n = x.len() as f64; - let avg = mean(x); - let sq_diffs: f64 = x - .iter() - .map(|xi| (xi - avg).powi(2)) - .sum(); - sq_diffs / (n - 1.0) -} - -fn main() { - let x = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]; - println!("Variance is: {:.2}", variance(&x)); -} -``` - -
- - -## Exercise 2 - -Create a function `standardize()` to perform z-score standardization on a vector of `f64`. - -$$ -z_i = \frac{x_i - \mu}{\sigma} -$$ - -* Use `.iter()` and `.map()` to calculate mean and variance. -* Use `.into_iter()`, `.map()`, and `.collect()` to build the standardized vector. -* Return a new `Vec` of standardized values. - -### Solution - -
-View solution - -```rust -fn standardize(x: &[f64]) -> Vec { - let avg = mean(x); - let std_dev = variance(x).sqrt(); - x.iter().map(|xi| (xi - avg) / std_dev).collect() -} - -fn main() { - let x = vec![1.0, 2.0, 3.0, 4.0, 5.0]; - let standardized = standardize(x); - println!("Standardized: {:?}", standardized); -} -``` - -
diff --git a/intro-rust/iterators.qmd b/intro-rust/iterators.qmd index e25ee4c..d0d3588 100644 --- a/intro-rust/iterators.qmd +++ b/intro-rust/iterators.qmd @@ -1,152 +1,184 @@ -# Iterators +--- +title: "Iterators" +engine: knitr +--- -::: callout-tip +So far we have been using `for` loops to work through collections, but Rust has +a more powerful and flexible mechanism: iterators. Iterators produce a sequence +of items that can be accessed one at a time and chained with methods like +`.sum()`, `.min()`, `.max()`, and `.enumerate()`. They are closer in spirit to +`apply()` or `purrr::map()` than to a traditional for-loop. -## Objective - -- Understand what an iterator is and how to create one. -- Learn the difference between `.iter()` and `.into_iter()`. -- Use basic iterator methods like `.sum()`, `.min()`, `.max()`, and `.enumerate()`. - -::: - -So far we have been using a `for` loop to iterate through our items. However, it is not the only way to iterate. Instead, we can use **iterators**. Iterators often are simpler and easier to use—particularly when modifying each element in a `Vec`. - -Iterators can be created from basic collections like vectors or arrays as well as more advanced types such as `HashMap`s. They produce a sequence of items that can be accessed (or modified) one at a time. +And actually, we were technically using iterators all along, since Rust calls +`.into_iter()` on the vector passed to a for-loop under the hood. Both of these +are equivalent: +```rust +let nums = vec![3, 6, 9]; -## Consuming vs. Borrowing +for n in nums { ... } +for n in nums.into_iter() { ... } +``` -When we use a for loop with a collection, we are consuming (moving) the underlying vector. Under the hood, using a for loop actually _creates_ an iterator by calling the `.into_iter()` method. +## Borrowing -`for xi in x` is identical to calling `for xi in x.into_iter()` for example: +One important constraint on the use of `.into_iter()` is that it actually +*consumes* the vector — ownership is moved into the iterator so that the vector +can no longer be used afterward: -```rust +```{.rust filename="src/main.rs"} fn main() { let nums = vec![3, 6, 9]; - - for n in nums.into_iter() { + for n in nums.into_iter() { // ⬅️ nums is moved println!("Value: {n}"); } - - // nums has been moved ❌ - // println!("nums: {:?}", nums); + + println!("nums: {:?}", nums); // ❌ compiler error } ``` -Remember: `.into_iter()` *consumes* the original value. +To iterate without consuming the original collection, use `.iter()` instead. +This *borrows* the collection and produces references to each element, leaving +the original value intact: -Alternatively, the `.iter()` method can be called which **borrows** the collection and produces an iterator where each item is a refernce. - -```rust +```{.rust filename="src/main.rs"} fn main() { let nums = vec![3, 6, 9]; - + for n in nums.iter() { - println!("Reference to value: {}", n); + println!("Reference to value: {n}"); } - // nums is still usable ✅ - println!("nums: {:?}", nums); + + println!("nums is still usable: {:?}", nums); } ``` -## Basic Iterator Methods - -Once you have an iterator, there are a number of methods that can be used. Some of the helpful ones are: - -- `.sum()` — Add all the values together. -- `.min()` — Find the smallest value. -- `.max()` — Find the largest value. -- `.enumerate()` — Pairs each value with its index. - - -::: aside -The `.min()` and `.max()` methods cannot reliably return the min/max of a `Vec` because of floating point weirdness. Instead they return `Option<>` which we will discuss later. Just _know_ that they exist. -::: +```default +Reference to value: 3 +Reference to value: 6 +Reference to value: 9 +nums is still usable: [3, 6, 9] +``` -The `sum()` method would've been nice earlier, huh 😉. The `.sum()` method needs to have a type specified when using it. So we can specify the type when assigning the variable such as: +Once you have an iterator, a number of useful methods are available. Here is an +example of the sum method: -```rust +```{.rust filename="src/main.rs"} fn main() { let nums = vec![2, 4, 8]; let total: i32 = nums.iter().sum(); - println!("Sum is: {}", total); + println!("Sum is: {total}"); } ``` -## Enumerated iterators - -Often it is quite nice to have the index associated with each item in an iterator. To do so, we can use `.enumerate()` after calling either `.iter()` or `.into_iter()`. This modifies each element to be a tuple with `(i, xi)`. - -::: callout-note -The `.enumerate()` method returns the type `usize` which is a special type of unsigned integer. You can cast them to integers or floats using `i as i32`, for example. -::: +```default +Sum is: 14 +``` -Rust is 0 indexed so the first value of the enumeration is going to be `0`. +## Enumerating -For example: +You can also `.enumerate()` over iterators to get the index and value of the +current element. These are returned as a tuple `(i, xi)` where `i` is the index +and `xi` is the value. The data type of the index `i` is `usize`, which can if +needed be cast to `i32` with `i as i32`. Note that Rust is zero-indexed! -```rust +```{.rust filename="src/main.rs"} fn is_even(x: i32) -> bool { x % 2 == 0 } fn main() { - let x = vec![2, 4, 8]; + let x = vec![1, 3, 5]; for (i, xi) in x.iter().enumerate() { if is_even(i as i32) { - println!("i is even with value {xi}"); + println!("Index {i} is even with value {xi}"); } } } ``` +```default +Index 0 is even with value 1 +Index 2 is even with value 5 +``` -## Exercise 1 +## Mapping -- Modify the `mean()` function to calculate the mean using `.iter()` -- Create a vector of 5 or more `f64` values -- Calculate the mean and print the result +Maybe the most useful method to apply to iterators is `.map()`, the Rust analog +of `purrr::map()` and the `apply()` family of functions. Here is an example that +squares each element of a vector: -### Solution +```rust +// providing explicit type annotation in one +// value so the compiler knows its f64 and not f32 +let x = vec![5.9_f64, 6.8, 4.5, 7.3, 6.2]; -
-View solution +x.iter().map(|xi| xi.powi(2)) +``` + +Using `.map()` returns another iterator, so we can chain operations over +iterators by using multiple `.map()` statements. ```rust -fn mean(x: &[f64]) -> f64 { - let total: f64 = x.iter().sum(); - total / x.len() as f64 -} +// providing explicit type annotation in one +// value so the compiler knows its f64 and not f32 +let x = vec![5.9_f64, 6.8, 4.5, 7.3, 6.2]; -fn main() { - let nums = vec![1.0, 2.0, 3.0, 4.0, 5.0]; - println!("Mean is: {}", mean(&nums)); -} +x.iter().map(|xi| xi.powi(2)).map(|xi| xi - 1.0) +``` + +## Closure + +In these examples we use a *closure* to modify each element of the iterator, +which is Rust's version of an anonymous function. The syntax of a closure is +`|arg| expression`, similar to R's recent adoption of `\(arg) expression`. Like +R's anonymous functions, they can also be multiple lines by wrapping the +expression in curly brackets: + +```rust +x.iter().map(|xi| { + let squared = xi.powi(2); + squared.sqrt() +}) ``` -
+When using `.enumerate().map()`, you get the Rust version of `purrr::imap()`. -## Exercise 2 +```rust +x.iter() + .enumerate() + .map(|(i, xi)| { + // use both i and xi here + }) +``` -- Print the index and value for each item in a vector for **only even** values +Notice that the iterator is now the tuple `(i, xi)` as before in the for-loop. -### Solution +## Collecting -
-View solution +In R, `purrr::map()` always returns a list, but in Rust, `.map()` always returns +another iterator, so getting back a concrete collection means you must `.collect()` +the values in the iterator. Rust cannot always infer the type you want to +collect, however, so you need to provide it explicitly: ```rust -fn main() { - let nums = vec![1, 2, 3, 4, 5, 6]; +let x_squared: Vec = x.iter().map(|xi| xi.powi(2)).collect(); +``` - for (i, n) in nums.iter().enumerate() { - if n % 2 == 0 { - println!("Index {i}: {n} is even"); - } - } -} +Alternatively, you can use the *turbofish* syntax `.collect::()` to +specify the type directly on `.collect()` rather than in the assignment: + +```rust +let x_squared = x.iter().map(|xi| xi.powi(2)).collect::>(); +``` + +In either case, you may use a general placeholders to encourage Rust to infer +the type, the syntax being `Vec<_>`: + +```rust +let x_squared: Vec<_> = x.iter().map(|xi| xi.powi(2)).collect(); +let x_squared = x.iter().map(|xi| xi.powi(2)).collect::>(); ``` -
+This lets Rust infer the element type while you specify the container type. \ No newline at end of file diff --git a/intro-rust/loops.qmd b/intro-rust/loops.qmd new file mode 100644 index 0000000..654e63d --- /dev/null +++ b/intro-rust/loops.qmd @@ -0,0 +1,81 @@ +--- +title: "Loops" +engine: knitr +--- + +In Rust, the easiest way to iterate over items in a collection is with a `for` +loop. The syntax should look familiar to R programmers. Each item in the +collection is bound to the name you provide before `in` at each iteration of the +loop: + +```rust +for value in collection { + // do something with value +} +``` + +As with control flow, the content of a for-loop must be delimited by curly +brackets, but the in-statement does not require parantheses. As an example, +consider a program for printing each number in a vector: + +```{.rust filename="src/main.rs"} +fn main() { + let nums = vec![1, 2, 3]; + + for n in nums { + println!("n is: {n}"); + } +} +``` + +Running `cargo run --quiet` (the `quiet` suppresses compiler logs) should print +the following to your terminal: + +```default +n is: 1 +n is: 2 +n is: 3 +``` + +In the context of for-loops, Rust's scoping rules are important to be aware of. +They are, in fact, much stricter than R's. Like R, values declared outside a +for-loop are accessible inside it, but unlike R, values created inside the loop +cannot be used outside it. In the following example, `greeting` is defined in +the outer scope and is accessible inside the loop: + +```{.rust filename="src/main.rs"} +fn main() { + let greeting = "Hi"; + let names = vec!["Alice", "Bob"]; + + for name in names { + println!("{greeting}, {name}!"); + } +} +``` + +```default +Hi, Alice! +Hi, Bob! +``` + +In contrast, `doubled` in the next example is defined inside the loop and cannot +be accessed after it ends: + +```{.rust filename="src/main.rs"} +fn main() { + let numbers = vec![1, 2, 3]; + + for n in numbers { + let doubled = n * 2; + println!("{n} doubled is {doubled}"); + } + + // ❌ `doubled` doesn't exist here + // println!("Last doubled: {}", doubled); +} +``` + +In the next section we will see how to get around this scoping rule using a +mutable variable declared in the outer scope to collect values produced inside +the loop. diff --git a/intro-rust/mutability.qmd b/intro-rust/mutability.qmd index 80f88a3..3c1121e 100644 --- a/intro-rust/mutability.qmd +++ b/intro-rust/mutability.qmd @@ -1,74 +1,56 @@ -# Mutability +--- +title: "Mutability" +engine: knitr +--- -::: callout-tip -Understand the difference between immutable and mutable variables. -::: - -In R everything is immutable (except environments). Immutable objects _cannot be altered_. - -::: aside - -Instead, when they are modified, they are copied. This is called copy-on-write (often referred to as **cow** semantics). - -::: - -In Rust, variables are immutable by default. This means once a value is assigned to a variable, it cannot be changed. To make a variable mutable, you must explicitly use the `mut` keyword. +In R, most objects are effectively immutable — when you modify one, R copies it +first. This is called copy-on-write semantics. Rust takes a stricter approach, +making variables immutable by default, and the compiler enforces this at compile +time. To make a variable mutable, you must declare it with the `mut` keyword: ```rust let mut x = 5; -x = 6; // ✅ works because x is mutable +x = 6; // works because x is mutable ``` -- Use `mut` when you need to change a variable after it is created. -- You do **not** re-bind the variable with `let` when changing its value. -- This helps catch bugs early by making data changes explicit. - +Without `mut`, attempting to reassign a variable is a compile error. This helps +catch bugs early by making data changes explicit. Note that when updating a +mutable variable, you do not re-bind it with `let` — you simply assign the new +value directly. -By requiring `mut`, the compiler ensures that accidental mutations are caught at compile time. +Mutability is especially useful for accumulating values across a loop. In the +previous section, we saw that variables defined inside a loop cannot be accessed +outside it. The solution is to declare a mutable variable in the outer scope and +update it inside the loop: -## Example - -Revisiting our loop from earlier: - -```rust +```{.rust filename="src/main.rs"} fn main() { - // create a vector let numbers = vec![1, 2, 3]; - // create a mutable value let mut doubled = 0; - // iterate through numbers to update doubled for n in numbers { doubled = n * 2; println!("{} doubled is {}", n, doubled); } - // ✅ compiles because `doubled` was declared - // _outside_ of the inner loop scope + // `doubled` is accessible here because it was declared outside the loop println!("Last doubled: {}", doubled); } ``` -## Exercise - -- Create a vector of 5 or more f64 values (`Vec`) -- Use a for loop to calculate the sum of the vector -- Calculate the mean from the sum of the vector and the length of the vector -- Print the result - -::: callout-tip -You can use `+=` shorthand to add and assign all at the same time. For example `x += 10` is equivalent to `x = x + 10`. -::: - -### Solution +```default +1 doubled is 2 +2 doubled is 4 +3 doubled is 6 +Last doubled: 6 +``` -
-View solution +You can also use `+=` to add and assign in one step — `x += 10` is equivalent to +`x = x + 10`. This makes computing running totals concise: -```rust +```{.rust filename="src/main.rs"} fn main() { let x = vec![1.0, 2.0, 3.0, 4.0]; - let n = x.len() as f64; let mut total = 0.0; @@ -79,4 +61,84 @@ fn main() { println!("The mean is: {}", total / n); } ``` -
+ +```default +The mean is: 2.5 +``` + +Like scalar variables, vectors must also be declared `mut` to modify them after +creation. You can create an empty mutable vector and let Rust infer the data +type based on how you go on to use it: + +```{.rust filename="src/main.rs"} +fn main() { + let mut names = Vec::new(); + names.push("Alice"); + names.push("Bob"); + println!("{:?}", names); +} +``` + +```default +["Alice", "Bob"] +``` + +Here, `Vec::new()` creates an empty vector, and `.push()` appends an element to +the end. Because the element appended is a character string, the Rust compiler +is able to infer the type of the names vector at compile time. With this +example, we have also introduced another method, namely `.push()`, which is +particularly useful, but there are, in fact, many ways to modify mutable vectors +in Rust. Let us mention just three here. + +**Clear** -- Instead of adding elements, you can remove all elements at once +with `.clear()`: + +```{.rust filename="src/main.rs"} +fn main() { + let mut nums = vec![1, 2, 3]; + nums.clear(); + println!("{:?}", nums); +} +``` + +```default +[] +``` + +**Sort** -- Vectors can be sorted in place with `.sort()`. Not all types support +ordering, but numeric types and strings do: + +```{.rust filename="src/main.rs"} +fn main() { + let mut x = vec![11, 3, 7, 10, 1]; + println!("x before sorting: {x:?}"); + x.sort(); + println!("x after sorting: {x:?}"); +} +``` + +```default +x before sorting: [11, 3, 7, 10, 1] +x after sorting: [1, 3, 7, 10, 11] +``` + +**Extend** -- To combine two vectors, use `.extend()`, which appends the +contents of one vector onto another: + +```{.rust filename="src/main.rs"} +fn main() { + let mut a = vec![1, 2]; + let b = vec![3, 4]; + a.extend(b); + println!("{:?}", a); +} +``` + +```default +[1, 2, 3, 4] +``` + +Note that the vector being extended must be `mut` and that the second vector is +moved into the first — it can no longer be used after the call. We will +elaborate on that last point more in the section on Rust's memory model, +specifically the concept of ownership. \ No newline at end of file diff --git a/intro-rust/mutable-vectors.qmd b/intro-rust/mutable-vectors.qmd deleted file mode 100644 index 6c3e363..0000000 --- a/intro-rust/mutable-vectors.qmd +++ /dev/null @@ -1,105 +0,0 @@ -# Mutable Vectors - -::: callout-tip -## Objective -Learn how to create and modify vectors with mutable operations. -::: - -In Rust, vectors (`Vec`) are growable arrays. To modify a vector after creating it, the vector itself must be declared as `mut`. - -::: callout-note -You will often see `` in Rust code or documentation. `T` is a way of saying "of any type." Seeing `Vec` can be read as "a vector of any type" or "generic over type `T`." -::: - -### Creating empty vectors - -You can create an empty vector and let Rust infer the type based on usage: - -```rust -fn main() { - let mut names = Vec::new(); - names.push("Alice"); - names.push("Bob"); - println!("{:?}", names); -} -``` - -- `Vec::new()` creates an empty vector. -- `.push()` adds an element to the end of a vector. - -### Clearing vectors - -You can remove all elements from a vector using `.clear()`: - -```rust -fn main() { - let mut nums = vec![1, 2, 3]; - nums.clear(); - println!("{:?}", nums); // prints [] -} -``` - -### Sorting vectors - -Use `.sort()` to sort a vector. Important to note that not all types can be sorted. - -```rust -fn main() { - let mut x = vec![11, 3, 7, 10, 1]; - println!("x before sorting: {x:?}"); - x.sort(); - println!("x after sorting: {x:?}"); -} -``` -``` -x before sorting: [11, 3, 7, 10, 1] -x after sorting: [1, 3, 7, 10, 11] -``` - -### Combining vectors - -Use `.extend()` to append one vector's contents to another: - -```rust -fn main() { - let mut a = vec![1, 2]; - let b = vec![3, 4]; - // note that we don't assign. - // Instead, `a` is modified in place - a.extend(b); - println!("{:?}", a); // prints [1, 2, 3, 4] -} -``` - -- `.extend()` adds the contents of another vector. -- The original vector must be `mut`. -- The second vector is **moved** into the first. -- `b` can no longer be used. - -## Exercise - -- Create an empty vector -- Append the values `1.0`, `2.0`, and `3.0` using `.push()` -- Clear the vector to make it empty -- Lastly, extend it with another vector e.g. `[4.0, 5.0]` -- Sort the vector -- Print the final result. - -### Solution - -
-View solution - -```rust -fn main() { - let mut x = Vec::new(); - x.push(1.0); - x.push(2.0); - x.push(3.0); - x.clear(); - x.extend(vec![4.0, 5.0]); - println!("{:?}", x); -} -``` - -
diff --git a/intro-rust/options.qmd b/intro-rust/options.qmd deleted file mode 100644 index 56975ca..0000000 --- a/intro-rust/options.qmd +++ /dev/null @@ -1,83 +0,0 @@ -# `Some()` or `None`? - -::: callout-tip -## Objective - -Understand how missingness is handled in Rust with `Option`. -- Understand how to handle missingness -- Use `Option` in an applied case -::: - -Rust doesn't have the concept of a `NULL` type. Null values are quite dangerous and have their own category of errors and security issues. - -Rust makes it impossible to have a null value by formalizing the concept of missingness through the `Option<>` - -## `Option` - -An Option is a special `enum` that has two possible values: - -```rust -enum Option { - Some(T), - None -} -``` - -When a value is missing, the option's variant is `None`. When the option contains _some value_, the value is contained _inside of_ the `Some(T)` variant. - - -## Accessing `Some()` value - -Since `Option` are "just" an enum, we can match on the variants to access the values. - -```rust -// create an Option that contains a value -let measure = Some(Measure::Euclidean); - -match measure { - Some(v) => println!("The measure is not missing!"), - None => println!("Oh no! The measure is missing"), -} -``` - -We put a placeholder value inside of `Some(v)` to create a binding to the inner value. It's like a function argument, it can be called whatever you want it to be. - -## Danger! `.unwrap()` & `.expect()` - -Sometimes dealing with options is a headache, particularly when we're in the early stages of developing. - -We can use `.unwrap()` or `.expect()` to grab the inner value of an option _without matching_. - -This is **dangerous!!** because we are ignoring the possibility of a `None`. Unwrapping on a `None` leads to a **panic**. Panics cause your program to abort. - -```rust -thread 'main' panicked at src/main.rs:4:41: -called `Option::unwrap()` on a `None` value -``` - - -## Exercise - -- Modify the `distance()` method to take an `Option<&Measure>` -- When `None` use euclidean distance, otherwise use the provided distance measure - -::: aside -The exercise does not involve `.unwrap()` because it's a bad habbit. Let's try and form good habbits from the outset if possible. -::: - -
-View solution -```rust -impl Point { - fn distance(&self, destination: &Self, measure: Option<&Measure>) -> f64 { - match measure { - Some(m) => match m { - Measure::Euclidean => self.euclidean_distance(destination), - Measure::Haversine => self.haversine_distance(destination), - }, - None => self.euclidean_distance(destination), - } - } -} -``` -
diff --git a/intro-rust/ownership.qmd b/intro-rust/ownership.qmd new file mode 100644 index 0000000..7ce1ace --- /dev/null +++ b/intro-rust/ownership.qmd @@ -0,0 +1,199 @@ +--- +title: "Ownership" +engine: knitr +--- + +Rust is notorious for its **borrow checker**, the mechanism that enforces memory +safety at compile time. The core rule is that every value has a single owner, +and once that ownership is transferred, the original variable can no longer be +used. This is called a **move**. A move occurs whenever a variable is passed to +a function or assigned to another variable. After the move, the original binding +is invalid. Consider this example from The Book: + +```{.rust filename="src/main.rs"} +fn main() { + let s1 = String::from("hello"); + let s2 = s1; + println!("{s1}, world!"); +} +``` + +Running this code will cause the following error: + +```default +error[E0382]: borrow of moved value: `s1` + --> src/main.rs:4:16 + | +2 | let s1 = String::from("hello"); + | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait +3 | let s2 = s1; + | -- value moved here +4 | println!("{s1}, world!"); + | ^^ value borrowed here after move + | + = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider cloning the value if the performance cost is acceptable + | +3 | let s2 = s1.clone(); + | ++++++++ + + +For more information about this error, try `rustc --explain E0382`. +``` + +There is a lot of output here, but let us focus on the source of the error for +now. First, we see that `let s2 = s1;` moves the value in `s1` to `s2`, and then +we are told that the `println!` macro mistakenly seeks to borrow the value that +has been moved. Since that value is no longer *owned* by `s1`, it causes an +error. + +## Cloning + +In the example above, we are also given helpful advice about how to fix the +error using `.clone()`, which copies the value rather than moving it. Cloning is +probably the simplest way to reuse the original value, as almost everything in +Rust can be cloned. + +```{.rust filename="src/main.rs"} +fn main() { + let s1 = String::from("hello"); + let s2 = s1.clone(); + println!("{s1}, world!"); +} +``` + +```default +hello, world! +``` + +## Functions + +The above example also highlights that the `println` macro seeks to *borrow* the +value in `s1`. In fact, any time you pass a variable to a function, its value is +moved, meaning the originally defined variable can no longer be used to access +the value outside of the function. For example, let's define a `mean()` +function: + +```rust +fn mean(x: Vec) -> f64 { + let mut total = 0.0; + let n = x.len(); + + for xi in x { + total += xi; + } + + total / (n as f64) +} +``` + +Calling this function twice on the same vector will fail because the first call +moves `x` into the function: + +```rust +let x = vec![0.0, 3.14, 10.1, 44.8]; + +let avg1 = mean(x); // ⬅️ x moved here! +let avg2 = mean(x); // ❌ compiler error +``` + +As we just saw, one way of handling this is to clone the data before passing it +to the function. + +```rust +let x = vec![0.0, 3.14, 10.1, 44.8]; + +let avg1 = mean(x.clone()); // ⬅️ x cloned here! +let avg2 = mean(x); // ✅ compiler happy +``` + +While cloning in this way is safe and correct, it has the unfortunate downside +of allocating a new copy of the data each time — not an issue for small vectors +like `x`, but as they grow, finding a better approach will become more urgent. + +## Borrowing + +Fortunately, there is a more efficient approach known as *borrowing*, which +involves passing a reference to a value rather than moving it. In Rust, we +signal a reference by placing an ampersand `&` in front of a variable or type +name, for example, `&x` or `&Vec`. These tell the compiler that we are +borrowing a value rather than taking ownership. To use the `mean()`function with +a reference, notice that we have to update its signature to make the reference +type explicit: + +```rust +fn mean(x: &Vec) -> f64 { + let mut total = 0.0; + let n = x.len(); + + for xi in x { + total += *xi; + } + + total / (n as f64) +} + +fn main() { + let x = vec![1.0, 2.0, 3.0]; + let avg = mean(&x); // 👈 borrowing `x` + println!("x is still usable: {:?}", x); +} +``` + +```default +x is still usable: [1.0, 2.0, 3.0] +``` + +An important restriction on referencing is that borrowed values cannot be moved +or mutated — only read. You can borrow a value as many times as you like, you +just cannot change it. + +## Slices + +Rather than reference the entire array or vector, we can reference a slice, or a +contiguous sequence of elements within it. These are denoted in Rust using `&[T]` +where `T` stands for a generic type. For example, `&[f64]` means a reference +slice of 64-bit floating points. If we want to demonstrate the use of reference +slices with our `mean()` function, we will as before have to update its +signature to make that type explicit: + +```{.rust filename="src/main.rs"} +fn mean(x: &[f64]) -> f64 { + let mut total = 0.0; + let n = x.len(); + + for xi in x { + total += *xi; + } + + total / (n as f64) +} + +fn main() { + let x = [0.0, 20.0, 742.3]; // array + let y = vec![1.0, 2.0, 3.0]; // vector + + println!("the mean of x is: {}", mean(&x)); + println!("the mean of y is: {}", mean(&y)); +} +``` + +```default +the mean of x is: 254.1 +the mean of y is: 2 +``` + +There are two things to note about this example. The first is very subtle. We +updated the signature for mean to take `x: &[f64]`, a slice. However, we passed +`&x` and `&y` to the function, which are references to arrays and vectors. What +is going on here? The answer is an implicit conversion. Both arrays and vectors +have methods for converting their generic references to slices, and those are +invoked when passing them into a function with this signature. + +Following from that, the second thing to note is that a function written to +accept `&Vec` works only on vectors. That is because there is no method +for converting array references to vector references. But as we just saw, both +can be converted to slices. Rewriting a function to accept `&[f64]`, thus, makes +it more general at no cost. So, in practice, we should prefer slices over vector +references whenever the function only needs to read the data — it is more +flexible and equally efficient. diff --git a/intro-rust/parallelize.qmd b/intro-rust/parallelize.qmd deleted file mode 100644 index c2759cb..0000000 --- a/intro-rust/parallelize.qmd +++ /dev/null @@ -1,15 +0,0 @@ -# Parallelizing - -TODO: before this we do combining iterators with .zip() and then go over destructuring into tuples there - -- this is a stretch exercise - -- Brief intro to rayon -- `par_iter()` -- `into_par_iter()` -- `with_min_len()` - - -- Objective is to calculate the pairwise distances between two vectors of points super fast -- we want to use `.zip()` to combine the two vectors -- this would require using rand to create a vector of random numbers diff --git a/intro-rust/references-slices.qmd b/intro-rust/references-slices.qmd deleted file mode 100644 index 5f14b25..0000000 --- a/intro-rust/references-slices.qmd +++ /dev/null @@ -1,190 +0,0 @@ -# Ownership - -::: callout-tip -## Objective - -Understand the how Rust ensures memory safety through the borrow checker and be able to avoid issues by borrowing, using a slice, or cloning. -::: - - -Rust is notorious for its **borrow checker**. The borrow check is Rust's secret sauce which helps ensure memory safety. At compile time, Rust looks through all of your code to enforce **ownership**. The key is that - -> ### variables can be used only once! - -If a variable has been used it has been **moved**. There are a number of ways to reuse variables. - -## Moves - -A move occurs whenever a variable is used by a function or a method. - -```rust -fn main() { - let x = vec![1.0, 2.0, 3.0]; - let y = x; // ⬅️ ownership moved - println!("{:?}", x); // ❌ error: value used after move -} -``` - -Since `x` was moved (used) by assigning it to `y`, the original value cannot be used. - - -## Cloning - -The simplest but least efficient way to reuse a variable is to clone it. This is what R does. - -::: callout-tip -Remember: when in doubt, clone it out! -::: - -Almost everything in Rust can be cloned by using the `.clone()` method. - -::: callout-important - -#### Cannot compile - -```rust -let x = v![0.0, 3.14, 10.1, 44.8]; - -let avg1 = mean(x); // ⬅️ x moved here! -let avg2 = mean(x); // ❌ compiler error -``` -::: - -If we first clone `x` before using `mean()` we can use `x` again. - -```rust -let x = v![0.0, 3.14, 10.1, 44.8]; - -let avg1 = mean(x.clone()); // ⬅️ x cloned here! -let avg2 = mean(x); // ✅ compiler happy -``` - - -## Borrowing - -While variables may only be used once, they can be **borrowed** infinitely (until they go out of scope). A variable can be used by **reference** when the `&` symbol is placed in front of it—e.g. `arg: &Vec`. - -```rust -fn main() { - let x = vec![1.0, 2.0, 3.0]; - let avg = mean(&x); // 👈 borrowing `x` - println!("x is still usable: {:?}", x); -} -``` - -If you borrow a variable you cannot mutate it or move it. - -## Slices - -Slices are a special type of borrowing. Slices are a reference to contiguous section of the same type. They're recognized by the syntax `&[T]`. - -Slices always have a known length (accessed via `.len()`) and can be used from more than one type. - -### Example - -Both an array `[f64; N]` and a `Vec` can be turned into a `&[f64]` - -```rust -let x = [0.0, 20.0, 742.3]; -let y = vec![1.0, 2.0, 3.0]; - -let avg_x = mean(&x) -let avg_y = mean(&y); -``` - -Slices are more flexible and more light-weight than borrowing a full vector and should be preferred whenever possible. - -In general: slice > reference > owned - - - - -## Exercise 1 - -- Calculate the mean of the same `Vec` twice -- Print the vector at the end - -
-View solution - -```rust -fn mean(x: Vec) -> f64 { - let mut total = 0.0; - for xi in x { - total += xi; - } - total / x.len() as f64 -} - -fn main() { - let x = vec![1.0, 2.0, 3.0, 4.0, 5.0]; - let result = mean(x.clone()); - let result2 = mean(x.clone()) - println!("The vector is still {:?}", x); -} -``` - -
- -## Exercise 2 - -- Rewrite the `mean()` function to accept a **reference** to a `Vec` instead of taking ownership -- Then call it with a borrowed vector. - - -
-View solution - -```rust -fn mean(x: &Vec) -> f64 { - let mut total = 0.0; - for xi in x { - total += xi; - } - total / x.len() as f64 -} - -fn main() { - let x = vec![1.0, 2.0, 3.0, 4.0, 5.0]; - let result = mean(&x); - println!("The mean of x is {result}"); -} -``` - -
- - -## Exercise 3 - -- Rewrite `mean()` to accept a slice (`&[f64]`) instead of a `Vec` reference. -- Create an array of `f64` values called `y` -- Calculate the mean on both `x` and `y` -- Print both averages - - -
-View solution - -```rust -fn mean(x: &[f64]) -> f64 { - let mut total = 0.0; - for xi in x { - total += xi; - } - total / x.len() as f64 -} - -fn main() { - let x = vec![1.0, 2.0, 3.0, 4.0, 5.0]; - let y = [0.0, 9.5, 3.3, 11.78, 3.14159]; - println!("The mean of x is {}.\nThe mean of y is {}", mean(&x), mean(&y)); -} -``` - -
diff --git a/intro-rust/struct-methods.qmd b/intro-rust/struct-methods.qmd deleted file mode 100644 index d5ebe1e..0000000 --- a/intro-rust/struct-methods.qmd +++ /dev/null @@ -1,193 +0,0 @@ -# Struct Methods - -::: callout-tip -## Objectives - -Learn how to implement methods for stucts. Be able to create associated functions and understand the difference between `Self`, `&self`, and `&mut self` - - -::: - -## Associated Functions - -Typically methods are used on instantiations of a variable such as: - -```rust -let me = Person { name: "Josiah".to_string(), age: 29 }; -let birth_year = me.year_of_birth(); -``` - -Sometimes it makes sense to use an **associated function**. These are functions that are called from the type itself e.g. `Type::function()`. They're typically used for *constructors*. For example - -```rust -let me = Person::new("Josiah".to_string(), 29); -``` - -They're created using an `impl` block. - -## `impl`ement methods - -`impl` is short for **implement**. We can implement methods for structs using the following syntax: - -```rust -impl TypeName { - fn method() { - todo!() - } -} -``` - -Associated functions are methods that do not reference the type itself. To create a `new()` constructor function that returns the type we can set the return type to `Self`: - -```rust -impl Person { - fn new(name: String, age: i32) -> Self { - Person { name, age } - } -} -``` - -This gives us the ability to use `Person::new()` - -## Self references - -However, it often makes sense to call methods _from_ an instantiation itself. - -We can do this by setting the first argument to `&self` which provides a **reference** to the struct the method is called on. - -```rust -impl Person { - fn greet(&self) { - println!("Hello, my name is {} and I am {} years old.", self.name, self.age); - } -} -``` - -Using a `&self` reference means you cannot _move_ the inner fields, only borrow them. - - -We can also have arguments that refer to other instantiations of the same type via `&Self` or `Self`. For example to compare ages of people: - -```rust -impl Person { - fn is_older_than(&self, other_person: &Self) -> bool { - self.age > other_person.age - } -} -``` - -## Mutable self - -Often it makes sense to modify the fields of a struct. This is when a _mutable reference_ is handy via `&mut self`. - -For example we can implement methods to `rename()` and `celebrate_birthday()`: - -```rust -impl Person { - fn rename(&mut self, new_name: &str) { - self.name = new_name.to_string(); - } - - fn celebrate_birthday(&mut self) { - self.age += 1; - } -} -``` - -## Section 2 - -## Another Section - -## Exercise - -- Implement a `Point::new()` associated function - - Using arguments `x: f64` and `y: f64` - - It should return `Self` -- Define a method `euclidean_distance()` which calculates the distance between itself and another `Point` - - Use arguments `&self` and `destination: &Self` - - Return `f64` - - Distance is calculated as $\sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$ - - Use `.powi(2)` to square a value and `x.sqrt()` to get the square root -- In `main.rs` create two `Point` structs and calculate the distance between them. - - -
- View solution - -```rust -impl Point { - fn new(x: f64, y: f64) -> Self { - Self { x, y } - } - - fn euclidean_distance(&self, destination: &Self) -> f64 { - let x_diff = (self.x - destination.x).powi(2); - let y_diff = (self.y - destination.y).powi(2); - - (x_diff + y_diff).sqrt() - } -} - -fn main() { - let a = Point::new(0.0, 5.0); - let b = Point::new(-10.0, 1.5); - let distance = a.euclidean_distance(&b); - println!("Distance between a and b is {distance}"); -} -``` -
- - - -## Bonus: Exercise 2 - - -Euclidean distance is useful only on a rectangle. What about on a sphere? Haversine distance is used to calculate the distance on a sphere. The Haversine formula computes the distance between two points on a sphere given their longitudes and latitudes. - -It is defined by the following (messy) equation: - - -$$a = \sin^2\left(\frac{\Delta\phi}{2}\right) + \cos\phi_1 \cdot \cos\phi_2 \cdot \sin^2\left(\frac{\Delta\lambda}{2}\right)$$ - -$$c = 2 \cdot \text{atan2}(\sqrt{a}, \sqrt{1-a})$$ - -$$d = R \cdot c$$ - -Where: - -- $\phi$ represents latitude (in radians) (use `.to_radians()`) -- $\lambda$ represents longitude (in radians) (use `.to_radians()`) -- $R$ is the Earth's radius (you can use `6_371_008_7714f64` meters as a mean radius) -- $\Delta\phi$ is the difference in latitude between the two points ($\phi_2 - \phi_1$) -- $\Delta\lambda$ is the difference in longitude between the two points ($\lambda_2 - \lambda_1$) -* $\text{atan2}(y, x)$ is the **arctangent** of $y/x$, using the signs of both arguments to determine the correct quadrant. - - -**Hints for Implementation:** - -* Remember to convert your latitude and longitude values from degrees to **radians** using the `.to_radians()` method on `f64`. -* The `c` part of the formula, $2 \cdot \text{atan2}(\sqrt{a}, \sqrt{1-a})$, can also be implemented using `2 * a.sqrt().asin()` in Rust, which is a common simplification when `a` is within the valid domain for `asin`. - - -
- View solution - -```rust -impl Point { - fn haversine_distance(&self, destination: &Self) -> f64 { - let radius = 6_371_008.7714; // Earth's mean radius in meters - let theta1 = self.y.to_radians(); // Latitude of point 1 - let theta2 = destination.y.to_radians(); // Latitude of point 2 - let delta_theta = (destination.y - self.y).to_radians(); // Delta Latitude - let delta_lambda = (destination.x - self.x).to_radians(); // Delta Longitude - - let a = (delta_theta / 2f64).sin().powi(2) - + theta1.cos() * theta2.cos() * (delta_lambda / 2f64).sin().powi(2); - - 2f64 * a.sqrt().asin() * radius - } -} - -``` - -
diff --git a/intro-rust/structs.qmd b/intro-rust/structs.qmd deleted file mode 100644 index b73940f..0000000 --- a/intro-rust/structs.qmd +++ /dev/null @@ -1,145 +0,0 @@ -# Defining Struct(ure)s - -::: callout-tip -## Objective - -Create new `struct` types and destructure them. Additionally, add behavior through `derive`-macros. -::: - -You can define a type as being a collection of other types by using the `struct` keyword. - - -```rust -struct Person { - name: String, - age: i32 -} -``` - -`struct`s are named using PascalCase, as opposed to `fn`s which are named using snake_case convention. Structs are constructed using a "literal" syntax. Where we write the name of the struct followed by curlys and the fields like so: - -```rust -let me = Person { name: "Josiah".to_string(), age: 29 }; -``` - -By default, new structs do not get any special behavior. Meaning they cannot be printed using `println!()`: - -```rust -struct Person { - name: String, - age: i32 -} - -fn main() { - let me = Person { name: "Josiah".to_string(), age: 29 }; - println!("{me:?}"); -} -``` -``` -error[E0277]: `Person` doesn't implement `Debug` - --> src/main.rs:8:15 - | -8 | println!("{me:?}"); - | ^^^^^^ `Person` cannot be formatted using `{:?}` - | - = help: the trait `Debug` is not implemented for `Person` - = note: add `#[derive(Debug)]` to `Person` or manually `impl Debug for Person` - = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) -help: consider annotating `Person` with `#[derive(Debug)]` - | -1 + #[derive(Debug)] -2 | struct Person { -``` - -## Deriving behavior - -Rust contains a special type called a **trait**. They're akin to S3 generics. They are a a collection of methods that can be implemented by foreign types. - -In this case, `Debug` is a trait that allows for debug printing using `println!()`. Our new type does not implement this by default. However, we can **derive** this behavior because everything inside of the struct _does_. - -To derive behavior we use the `#[derive()]` **attribute**. - -```rust -#[derive(Debug, Clone)] -struct Person { - name: String, - age: i32 -} -``` - -The `Debug` trait allows debug printing of the type `Person` via `dbg!()` or `println!("{p:?})`. - -```rust -fn main() { - let me = Person { name: "Josiah".to_string(), age: 29 }; - println!("{me:?}"); -} -``` -``` -Person { name: "Josiah", age: 29 } -``` - -## Accessing Fields - -Fields of a `struct` can be accessed directly, or by reference. Field can be accessed using `var.field_name`. - -If a field is accessed and moved, the `struct` cannot be moved. The same rules of borrowing apply to a struct. You can borrow a field as much as you want but you can only move it once. Otherwise, the entire struct cannot be used. - -```rust -fn main() { - let me = Person { name: "Josiah".to_string(), age: 29 }; - let name = me.name; - println!("{me:?}"); -} -``` -```rust -error[E0382]: borrow of partially moved value: `me` - --> src/main.rs:10:15 - | -9 | let name = me.name; - | ------- value partially moved here -10 | println!("{me:?}"); - | ^^^^^^ value borrowed here after partial move -``` - - -## Destructuring - -It is possible to **destructure** a struct during assignment creating many variables at one time. - -To destructure during assignment you use the syntax `let StructName { field, field } = variable;` - -```rust -fn main() { - let me = Person { name: "Josiah".to_string(), age: 29 }; - let Person { name, age } = me; - println!("{name} is {age} years old"); -} -``` - -## Exercise - -- Define a struct called `Point` which has two fields `x`, and `y` that are `f64` -- Derive `Debug` and `Clone` -- In `main()` create a new `Point` struct -- Debug print the new point -- Destructure the point into x and y - - -
- View solution - -```rust -#[derive(Debug, Clone)] -struct Point { - x: f64, - y: f64 -} - -fn main() { - let point = Point { x: 3.0, y: 0.14 }; - println!("The point is {:?}"); - let Point { x, y } = point; -} -``` -
diff --git a/intro-rust/structures.qmd b/intro-rust/structures.qmd new file mode 100644 index 0000000..23ab6c0 --- /dev/null +++ b/intro-rust/structures.qmd @@ -0,0 +1,168 @@ +--- +title: "Structures" +engine: knitr +--- + +Rust lets you define custom types by grouping related values together using the +`struct` keyword. This is similar in spirit to a named list in R, but with fixed +fields and a declared type. Struct names use PascalCase, while field names use +snake_case: + +```rust +struct Person { + name: String, + age: i32, +} +``` + +A struct is created using "literal" syntax, where we write the struct name +followed by curly brackets containing each field and its value: + +```rust +let me = Person { name: "Josiah".to_string(), age: 29 }; +``` + +## Derive + +By default, a new struct has no built-in behavior. It does not even have a +defined behavior for printing. So, right now, if you try to print the `Person` +struct, you will get an error. + +```{.rust filename="src/main.rs"} +struct Person { + name: String, + age: i32 +} + +fn main() { + let me = Person { name: "Josiah".to_string(), age: 29 }; + println!("{me:?}"); +} +``` + +```default +error[E0277]: `Person` doesn't implement `Debug` + --> src/main.rs:8:15 + | +8 | println!("{me:?}"); + | ^^^^^^ `Person` cannot be formatted using `{:?}` because it doesn't implement `Debug` + | + = help: the trait `Debug` is not implemented for `Person` + = note: add `#[derive(Debug)]` to `Person` or manually `impl Debug for Person` + = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider annotating `Person` with `#[derive(Debug)]` + | +1 + #[derive(Debug)] +2 | struct Person { + | + +For more information about this error, try `rustc --explain E0277`. +``` + +Here again we see a great demonstration of Rust's very informative error +messages. In this case, it also suggests a solution. We should derive the +`Debug` trait. What is a trait exactly? In Rust, traits are similar to R's S3 +generics — they define a set of methods that a type can support. In this case, +the `Debug` trait allows for printing the content of a struct to stdout using +the special `:?` syntax in `println!()` or the `dbg!()` macro. + +For very simple behaviors like printing, it is possible to derive the behavior +automatically from the struct definition itself. In fact, this is what the error +message above is telling us to do. It tells us to use another type of macro, +specifically the `derive` macro, placing it above the struct definition and +telling it what specific behavior to derive, in this case, `Debug`. + +```{.rust filename="src/main.rs"} +#[derive(Debug)] +struct Person { + name: String, + age: i32 +} + +fn main() { + let me = Person { name: "Josiah".to_string(), age: 29 }; + println!("{me:?}"); +} +``` + +```default +Person { name: "Josiah", age: 29 } +``` + +## Fields + +Fields in a struct can be accessed with dot notation: `me.name`, `me.age`. But +beware that ownership rules apply here, too. Accessing a field by moving it out +of the struct partially moves the struct itself, making the whole struct +unusable afterward: + +```{.rust filename="src/main.rs"} +#[derive(Debug)] +struct Person { + name: String, + age: i32, +} + +fn main() { + let me = Person { name: "Josiah".to_string(), age: 29 }; + let name = me.name; // ⬅️ name moved out + println!("{me:?}"); // ❌ error: partial move +} +``` + +```default +error[E0382]: borrow of partially moved value: `me` + --> src/main.rs:10:16 + | + 9 | let name = me.name; // name moved out + | ------- value partially moved here +10 | println!("{me:?}"); // error: partial move + | ^^ value borrowed here after partial move + | + = note: partial move occurs because `me.name` has type `String`, which does not implement the `Copy` trait + = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) + +For more information about this error, try `rustc --explain E0382`. +``` + +To read a field without consuming it, borrow it instead by referencing the +struct. + +```{.rust filename="src/main.rs"} +#[derive(Debug)] +struct Person { + name: String, + age: i32, +} + +fn main() { + let me = Person { name: "Josiah".to_string(), age: 29 }; + let name = &me.name; + println!("{me:?}"); +} +``` + +```default +Person { name: "Josiah", age: 29 } +``` + +## Destructuring + +Rust lets you unpack all fields of a struct in a single `let` binding. This is +called destructuring and uses the syntax `let StructName { field1, field2 } = variable`: + +```{.rust filename="src/main.rs"} +fn main() { + let me = Person { name: "Josiah".to_string(), age: 29 }; + let Person { name, age } = me; + println!("{name} is {age} years old"); +} +``` + +```default +Josiah is 29 years old +``` + +Destructuring is useful when you need several fields at once and want to avoid +repeated `.field` access. The struct is consumed in the process, so the original +variable is no longer available. diff --git a/intro-rust/types.qmd b/intro-rust/types.qmd index 12b015e..475f15e 100644 --- a/intro-rust/types.qmd +++ b/intro-rust/types.qmd @@ -1,114 +1,81 @@ -# Basic Types - -::: callout-tip -## Objective - -Familiarize yourself with basic types in Rust and when they may be used. - -::: - -In R everything is a vector. A vector is a **collection** of values. There is no **scalar** type for a double/ character / integer / logical vector. Those are just length 1 vectors. - -In Rust, however, scalars are the building blocks of everything. When a collection is needed those are made directly from scalars. You will often hear of these as **primitives**. - -Today we will only be using a handful of these primitives but it is important to understand what they are—in general. - - - -## Integers - -Integers are either **signed** or **unsigned**. - -A signed integer can contain a _negative_ value. Whereas an unsigned integer can contain only positive numbers. - - -- Signed Integers: `i8`, `i16`, `i32`, `i64`, `i128` -- Unsigned Integers: `u8`, `u16`, `u32`, `u64`, `u128` - - -::: callout-note -In R, an integer vector is comprised of `i32` values. -::: - -The letter prefix is the type of primitive. The following number indicates how many bits can be used to store the values. Unsigned integers can contain more possible values because they do not have to support negative numbers. - -```rust +--- +title: "Basic Types" +engine: knitr +--- + +In R, everything is a vector or a *collection* of values. There is no concept of +a single value variable or scalar. The closest equivalent to that is a +length-one vector. In Rust, however, scalars are the building blocks of +everything. When a collection is needed, it is made directly from scalars. For +this reason, you will often hear them referred to as **primitives**. A +primitive or scalar can be used to hold a single value of a specific data type, +including strings, integers, and floats, among others. In this tutorial, we will +cover some of those data types, focusing on how to instantiate them and convert +between them. + +Integers in Rust are a good place to start. They can be either signed, meaning +they can represent negative values, or unsigned, meaning they support only +positive values. Both varieties come in several sizes depending on how many bits +are needed to store the value: + +- Signed integers: `i8`, `i16`, `i32`, `i64`, `i128` +- Unsigned integers: `u8`, `u16`, `u32`, `u64`, `u128` + +The letter prefix indicates the type (`i` for signed, `u` for unsigned) and the +number indicates the bit width. Unsigned integers can hold larger positive +values than their signed counterparts because they do not need to use a bit to +represent a negative value: + +``` rust i32::MAX // 2147483647 u32::MAX // 4294967295 ``` -## Floating point - -In many cases where we need to do math we will use a floating point number. Floating points are **signed** and allow for decimal values. - -There are two types: `f32` and `f64`. +Floating points are similar to integers but can represent decimal values, too. +Unlike integers, floating points are also always signed and come in two sizes: +`f32` and `f64`. -::: callout-note -In R, a double vector is comprised of `f64` values. +::: {.callout-note} +In R, integer vectors are comprised of only `i32` values, and floating points, +called doubles, are always `f64` values. ::: -## Strong typing +Rust can usually infer types. For instance, it can distinguish the integer `1` +from the floating point `1.0`, but you can also specify the type explicitly. +There are two ways to do this — using `:` in the assignment, or by appending a +type suffix to the literal value: +``` rust +let x: f64 = 10.0; +let x = 10.0f64; +``` -Rust will infer a value's type exceptionally well! However, we may want to specify the type manually as well. We can do this 2 primary ways: - -- In assignment using `:` for example `let x: f64 = 10;` -- Or by specifying the suffix e.g. `10f64` or `10_i32` - -::: callout-tip -You can use `_` as a visual separator when specifying numbers in Rust. The following are all identical values: +You can also use `_` as a visual separator when writing numeric literals. The +following are all identical: -```rust +``` rust let x: i32 = 1000; let x = 1000i32; let x = 1_000_i32; ``` -::: - -## Type casting - -In Rust, math expressions can only be performed between like-types. You **cannot** add `2.0 + 3_f64`. To accomplish this we must **cast** to the same types. -Primitive types can be cast into one another using the `as` keyword. To add an `f64` to an `i32` we should ensure they're the same type. - -```rust +This pattern of explicitly declaring the type of a variable may seem somewhat +verbose to the average R user since R is very lax about types, relying as it +does on a lot of implicit conversions. Becuase of Rust's memory management +model, however, Rust is a strongly, statically typed language. That means that +all types must be known when you go to build or compile your crate. It also +precludes running operations on different types. For example, math expressions +can only be performed between values of the same type, so you cannot, for +example, directly add an `f64` and an `i32`. You must first cast one to match +the type of the other, in the simplest case using the `as` keyword: + +``` rust fn add2(x: f64, y: i32) -> f64 { - x + y as f64 -} -``` - -## Unit type - -In Rust there is no concept of `NULL`. Instead there is the **unit type** which is represented as `()`. - -::: aside -The unit type is _technically_ a tuple without any fields. If the internet is to be believed it is called "unit" becaue it can only have one value and is related to the "singleton" or unit set in set theory. -::: - -Functions that do not return anything _technically_ return `()`. - -## Exercise - -Modify `src/main.rs` to add two or more incompatible types. - -- Define a variable `x` to be an `f64` value -- Define `y` to be an `i32` value -- Add them up and store them in the variable `z` -- Use `println!()` to print the value of `z` - - -### Solution - -
- View solution - -```rust -fn main() { - let x = 3.14; - let y = 47_i32; - let z = x + y as f64; - println!(" The value of z is {z}"); + x + (y as f64) } ``` -
+If you call `add2(3.2, 2)`, the function will first cast `y` to `f64` before +doing addition, returning the `f64` value `5.2`. Before we continue, note that +Rust supports all the same numeric operators as R: `+`, `-`, `/`, `*`, and `%` +for remainder (equivalent to `%%` in R). \ No newline at end of file diff --git a/intro-rust/why-rust.qmd b/intro-rust/why-rust.qmd deleted file mode 100644 index bcdd03f..0000000 --- a/intro-rust/why-rust.qmd +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Why Rust? ---- - -::: callout-tip -## Objective - -Provide motivation for _why_ learning Rust is a use of our time. Discuss _what_ Rust is and how it is unique compared to other compiled langauges. - -🧠 Think of Rust as "C for people who like error messages that actually help." -::: - -## Reasoning - -Rust is a programming language that’s fast, safe, and surprisingly friendly to use. Rust turns your code into a standalone program that runs directly on your computer. This makes it much faster and more efficient, similar to languages like C or C++. But where those languages can be hard to use and easy to break, Rust was built to be safer and more helpful. - -Rust is especially good at preventing bugs related to memory. There's no garbage collector in Rust so it is quite memory efficient. Moreover, Rust is designed with the developer in mind. The rust compiler provides error messages that rival—or maybe even surpass—the quality of tidyverse error messaging. - - -## TL;DR - -- R is interpreted. Code runs line by line. -- R (and Python) are built on C. -- C is compiled — it builds a binary that runs directly on your machine. -- Rust is also compiled, like C, C++, Go, Java, and Fortran. -- These languages are "close to the metal" — fast, efficient, and powerful. -- Rust matches C++ in speed, but with some key advantages: - - **Memory safety** without a garbage collector (unlike Go), thanks to the borrow checker. - - A great developer experience: - - Helpful, friendly compiler errors (Tidyverse-level DX). - - Modern tooling (cargo, rustdoc, rustfmt, *rust-analyzer*, etc.). -- Rust is easy to get started with and rewards best practices. - - We won’t go deep into memory safety or concurrency today -- Rust uses errors as data, unlike R and C++, Rust does not support exceptions diff --git a/js/breadcrumb-fix.js b/js/breadcrumb-fix.js deleted file mode 100644 index 55491e5..0000000 --- a/js/breadcrumb-fix.js +++ /dev/null @@ -1,7 +0,0 @@ -// Remove Quarto breadcrumb classes to allow custom styling -document.addEventListener('DOMContentLoaded', function() { - const breadcrumbNavs = document.querySelectorAll('nav.quarto-page-breadcrumbs.quarto-title-breadcrumbs'); - breadcrumbNavs.forEach(function(nav) { - nav.classList.remove('quarto-page-breadcrumbs', 'quarto-title-breadcrumbs', 'd-none', 'd-lg-block'); - }); -}); \ No newline at end of file diff --git a/user-guide/error-handling/basic-error-handling.qmd b/user-guide/_drafts/basic-error-handling.qmd similarity index 100% rename from user-guide/error-handling/basic-error-handling.qmd rename to user-guide/_drafts/basic-error-handling.qmd diff --git a/user-guide/type-mapping/characters.qmd b/user-guide/_drafts/characters.qmd similarity index 100% rename from user-guide/type-mapping/characters.qmd rename to user-guide/_drafts/characters.qmd diff --git a/user-guide/type-mapping/collections.qmd b/user-guide/_drafts/collections.qmd similarity index 100% rename from user-guide/type-mapping/collections.qmd rename to user-guide/_drafts/collections.qmd diff --git a/user-guide/cran-msrv.qmd b/user-guide/_drafts/cran-msrv.qmd similarity index 100% rename from user-guide/cran-msrv.qmd rename to user-guide/_drafts/cran-msrv.qmd diff --git a/user-guide/cran-publishing.qmd b/user-guide/_drafts/cran-publishing.qmd similarity index 100% rename from user-guide/cran-publishing.qmd rename to user-guide/_drafts/cran-publishing.qmd diff --git a/user-guide/default-args.qmd b/user-guide/_drafts/default-args.qmd similarity index 100% rename from user-guide/default-args.qmd rename to user-guide/_drafts/default-args.qmd diff --git a/user-guide/_drafts/extendr-macro.qmd b/user-guide/_drafts/extendr-macro.qmd new file mode 100644 index 0000000..145567b --- /dev/null +++ b/user-guide/_drafts/extendr-macro.qmd @@ -0,0 +1,423 @@ +--- +title: "The `#[extendr]` macro" +execute: + eval: false +--- + +The central task of extendr is to make Rust code available in R. Crucial to this +effort is the attribute macro, `#[extendr]`. This chapter provides a brief +introduction to the use of that macro. + +::: {.callout-note} +If you would like to learn more about Rust macros, check The Book, specifically +[Part 5 of Ch. 20](https://doc.rust-lang.org/book/ch20-05-macros.html). You may +also want to consult the section on [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html) +in the official Rust reference manual. +::: + +```{r} +#| include: false +library(rextendr) +``` + +Rust allows attribute macros to be placed on functions, struct, enums, and +impls, and extendr supports all of these. The general pattern is mostly the same +in each case, but for starters, let's consider how to apply the extendr macro to +a Rust function in order to make it available in R. In order for that to happen, +three things need to happen. First, you need to declare the extendr namespace to +the compiler with `use`. After that, you need to annotate whatever function you +want to make available in R with the `#[extendr]` macro. Finally, you must +register the function in the `extendr_module! {}` macro. + +As an example, suppose that you have created a function that gives the answer to +the question "What is the meaning of life?" and that you have very thoughtfully +decided to share this wisdom with the R community. Your main Rust library script +should then look like this: + +```{.rust filename="src/lib.rs"} +use extendr_api::prelude::*; + +#[extendr] +fn answer_to_life() -> i32 { + 42 +} + +extendr_module! { + mod hitchhiker; + fn answer_to_life; +} +``` + +Here, the `#[extendr]` macro is placed directly above the function definition. + + +## `ToVectorValue` trait + +In order for an item to be returned from a function marked with the `#[extendr]` +attribute macro, it must be able to be turned into an R object. In extendr, the +struct `Robj` is a catch all for any type of R object. + +::: callout-note +For those familiar with PyO3, the `Robj` struct is similar in concept to the +[`PyAny`](https://docs.rs/pyo3/latest/pyo3/types/struct.PyAny.html) struct. +::: + +The `ToVectorValue` trait is what is used to convert Rust items into R objects. +The trait is implemented on a number of standard Rust types such as `i32`, +`f64`, `usize`, `String` and more (see [all foreign implementations +here](https://extendr.github.io/extendr/extendr_api/robj/into_robj/trait.ToVectorValue.html#foreign-impls)) +which enables these functions to be returned from a Rust function marked with +`#[extendr]`. + +::: callout-note +In essence, all items that are returned from a function must be able to be +turned into an `Robj`. Other extendr types such as `List`, for example, have a +`From for Robj` implementation that defines how it is converted into an +`Robj`. +::: + +This means that with a little extra work, the `Shape` enum can be returned to R. +To do so, the `#[extendr]` macro needs to be added to an impl block. + +## Exporting `impl` blocks + +The other supported item that can be made available to R is an +[`impl`](https://doc.rust-lang.org/std/keyword.impl.html) block. `impl` is a +keyword that allows you to *implement* a trait or an inherent implementation. +The `#[extendr]` macro works with inherent implementations. These are `impl`s on +a type such as an `enum` or a `struct`. extendr *does not* support using +`#[extendr]` on trait impls. + +::: callout-note +You can only add an inherent implementation on a type that you have own and not +provided by a third party crate. This would violate the [orphan +rules](https://github.com/Ixrec/rust-orphan-rules?tab=readme-ov-file#what-are-the-orphan-rules). +::: + +Continuing with the `Shape` example, this enum alone cannot be returned to R. +For example, the following code will result in a compilation error + +```rust +#[derive(Debug)] +enum Shape { + Triangle, + Rectangle, + Pentagon, + Hexagon, +} + +#[extendr] +fn make_shape(shape: &str) -> Shape { + match shape { + "triangle" => Shape::Triangle, + "rectangle" => Shape::Rectangle, + "pentagon" => Shape::Pentagon, + "hexagon" => Shape::Hexagon, + &_ => unimplemented!() + } +} +``` + +``` +error[E0277]: the trait bound `Shape: ToVectorValue` is not satisfied + --> src/lib.rs:19:1 + | +19 | #[extendr] + | ^^^^^^^^^^ the trait `ToVectorValue` is not implemented for `Shape`, which is required by `extendr_api::Robj: From` + | +``` + +However, you add the `#[extendr]` attribute to the `Shape` enum, it can be +returned to R. + +```{extendrsrc} +#[derive(Debug)] +#[extendr] +enum Shape { + Triangle, + Rectangle, + Pentagon, + Hexagon, +} + +#[extendr] +fn make_shape(shape: &str) -> Shape { + match shape { + "triangle" => Shape::Triangle, + "rectangle" => Shape::Rectangle, + "pentagon" => Shape::Pentagon, + "hexagon" => Shape::Hexagon, + &_ => unimplemented!() + } +} + +``` + +It is also possible to add methods to structs/enums and their instances: + +```{extendrsrc} +#[derive(Debug)] +#[extendr] +enum Shape { + Triangle, + Rectangle, + Pentagon, + Hexagon, +} + +#[extendr] +impl Shape { + fn new(x: &str) -> Self { + match x { + "triangle" => Self::Triangle, + "rectangle" => Self::Rectangle, + "pentagon" => Self::Pentagon, + "hexagon" => Self::Hexagon, + &_ => unimplemented!(), + } + } + + fn n_coords(&self) -> usize { + match &self { + Shape::Triangle => 3, + Shape::Rectangle => 4, + Shape::Pentagon => 4, + Shape::Hexagon => 5, + } + } +} +``` + +In this example two new methods are added to the `Shape` enum. The first `new()` +is like the `make_shape()` function that was shown earlier: it takes a `&str` +and returns an enum variant. Now that the enum has an `impl` block with +`#[extendr]` attribute macro, it can be exported to R by inclusion in the +`extendr_module! {}` macro. + +```rust +extendr_module! { + mod hellorust; + impl Shape; +} +``` + +Doing so creates an environment in your package called `Shape`. The environment +contains all of the methods that are available to you. + +::: callout-tip +There are use cases where you may not want to expose any methods but do want to +make it possible to return a struct or an enum to the R. You can do this by +adding an empty impl block with the `#[extendr]` attribute macro. +::: + +If you run `as.list(Shape)` you will see that there are two functions in the +environment which enable you to call the methods defined in the impl block. You +might think that this feel like an [R6 +object](https://r6.r-lib.org/articles/Introduction.html) and you'd be right +because an R6 object essentially is an environment! + +```{r} +as.list(Shape) +``` + +Calling the `new()` method instantiates a new enum variant. + +```{r} +tri <- Shape$new("triangle") +tri +``` + +The newly made `tri` object is an [external +pointer](https://cran.r-project.org/doc/manuals/R-exts.html#External-pointers-and-weak-references) +to the `Shape` enum in Rust. This pointer has the same methods as the Shape +environment—though they cannot be seen in the same way. For example you can run +the `n_coords()` method on the newly created object. + +```{r} +tri$n_coords() +``` + +::: callout-tip +To make the methods visible to the `Shape` class you can define a `.DollarNames` +method which will allow you to preview the methods and attributes when using the +`$` syntax. This is very handy to define when making an impl a core part of your +package. + +```{r} +.DollarNames.Shape = function(env, pattern = "") { + ls(Shape, pattern = pattern) +} +``` +::: + +### `impl` ownership + +Adding the `#[extendr]` macro to an impl allows the struct or enum to be made +available to R as an external pointer. Once you create an external pointer, that +is then owned by R. So you can only get references to it or mutable references. +If you need an owned version of the type, then you will need to clone it. + +## Accessing exported `impl`s from Rust + +Invariably, if you have made an impl available to R via the `#[extendr]` macro, +you may want to define functions that take the impl as a function argument. + +Due to R owning the `impl`'s external pointer, these functions cannot take an +owned version of the impl as an input. For example trying to define a function +that subtracts an integer from the `n_coords()` output like below returns a +compiler error. + +```rust +#[extendr] +fn subtract_coord(x: Shape, n: i32) -> i32 { + (x.n_coords() as i32) - n +} +``` + +``` +the trait bound `Shape: extendr_api::FromRobj<'_>` is not satisfied + --> src/lib.rs:53:22 + | + | fn subtract_coord(x: Shape, n: i32) -> i32 { + | ^^^^^ the trait `extendr_api::FromRobj<'_>` is not implemented for `Shape` + | +help: consider borrowing here + | + | fn subtract_coord(x: &Shape, n: i32) -> i32 { + | + + | fn subtract_coord(x: &mut Shape, n: i32) -> i32 { + | ++++ +``` + +As most often, the compiler's suggestion is a good one. Use `&Shape` to use a +reference. + +## `ExternalPtr`: returning arbitrary Rust types + +In the event that you need to return a Rust type to R that doesn't have a +compatible impl or is a type that you don't own, you can use `ExternalPtr`. +The `ExternalPtr` struct allows any item to be captured as a pointer and +returned to R. + +Here, for example, an `ExternalPtr` is returned from the `shape_ptr()` +function. + +::: callout-tip +Anything that is wrapped in `ExternalPtr` must implement the `Debug` trait. +::: + +```{extendrsrc} +#[derive(Debug)] +enum Shape { + Triangle, + Rectangle, + Pentagon, + Hexagon, +} + +#[extendr] +fn shape_ptr(shape: &str) -> ExternalPtr { + let variant = match shape { + "triangle" => Shape::Triangle, + "rectangle" => Shape::Rectangle, + "pentagon" => Shape::Pentagon, + "hexagon" => Shape::Hexagon, + &_ => unimplemented!(), + }; + + ExternalPtr::new(variant) +} +``` + +Using an external pointer, however, is far more limiting than the `impl` block. +For example, you cannot access any of its methods. + +```{r, error = TRUE} +tri_ptr <- shape_ptr("triangle") +tri_ptr$n_coords() +``` + +To use an `ExternalPtr`, you have to go through a bit of extra work for it. + +```{extendrsrc, include = FALSE} +#[derive(Debug)] +enum Shape { + Triangle, + Rectangle, + Pentagon, + Hexagon, +} + +#[extendr] +impl Shape { + fn new(x: &str) -> Self { + match x { + "triangle" => Self::Triangle, + "rectangle" => Self::Rectangle, + "pentagon" => Self::Pentagon, + "hexagon" => Self::Hexagon, + &_ => unimplemented!(), + } + } + + fn n_coords(&self) -> usize { + match &self { + Shape::Triangle => 3, + Shape::Rectangle => 4, + Shape::Pentagon => 4, + Shape::Hexagon => 5, + } + } +} + +#[extendr] +fn shape_ptr(shape: &str) -> ExternalPtr { + let variant = match shape { + "triangle" => Shape::Triangle, + "rectangle" => Shape::Rectangle, + "pentagon" => Shape::Pentagon, + "hexagon" => Shape::Hexagon, + &_ => unimplemented!(), + }; + + ExternalPtr::new(variant) +} + +#[extendr] +fn n_coords_ptr(x: Robj) -> i32 { + let shape = TryInto::>::try_into(x); + + match shape { + Ok(shp) => shp.n_coords() as i32, + Err(_) => 0 + } +} +``` + +```rust +#[extendr] +fn n_coords_ptr(x: Robj) -> i32 { + let shape = TryInto::>::try_into(x); + + match shape { + Ok(shp) => shp.n_coords() as i32, + Err(_) => 0 + } +} +``` + +This function definition takes an `Robj` and from it, tries to create an +`ExternalPtr`. Then, if the conversion did not error, it returns the +number of coordinates as an `i32` (R's version of an integer) and if there was +an error converting, it returns 0. + +```{r} +tri_ptr <- shape_ptr("triangle") + +n_coords_ptr(tri_ptr) + +n_coords_ptr(list()) +``` + +For a good example of using `ExternalPtr` within an R package, refer to the +[`b64` R package](https://github.com/extendr/b64). \ No newline at end of file diff --git a/user-guide/faq.qmd b/user-guide/_drafts/faq.qmd similarity index 100% rename from user-guide/faq.qmd rename to user-guide/_drafts/faq.qmd diff --git a/user-guide/type-mapping/into-list.qmd b/user-guide/_drafts/into-list.qmd similarity index 100% rename from user-guide/type-mapping/into-list.qmd rename to user-guide/_drafts/into-list.qmd diff --git a/user-guide/type-mapping/missing-values.qmd b/user-guide/_drafts/missing-values.qmd similarity index 100% rename from user-guide/type-mapping/missing-values.qmd rename to user-guide/_drafts/missing-values.qmd diff --git a/user-guide/r-pkgs/package-setup.qmd b/user-guide/_drafts/package-setup.qmd similarity index 100% rename from user-guide/r-pkgs/package-setup.qmd rename to user-guide/_drafts/package-setup.qmd diff --git a/user-guide/type-mapping/scalars.qmd b/user-guide/_drafts/scalars.qmd similarity index 100% rename from user-guide/type-mapping/scalars.qmd rename to user-guide/_drafts/scalars.qmd diff --git a/user-guide/serde-integration.qmd b/user-guide/_drafts/serde-integration.qmd similarity index 100% rename from user-guide/serde-integration.qmd rename to user-guide/_drafts/serde-integration.qmd diff --git a/user-guide/tokio.qmd b/user-guide/_drafts/tokio.qmd similarity index 100% rename from user-guide/tokio.qmd rename to user-guide/_drafts/tokio.qmd diff --git a/user-guide/type-mapping/vectors.qmd b/user-guide/_drafts/vectors.qmd similarity index 100% rename from user-guide/type-mapping/vectors.qmd rename to user-guide/_drafts/vectors.qmd diff --git a/user-guide/webr.qmd b/user-guide/_drafts/webr.qmd similarity index 100% rename from user-guide/webr.qmd rename to user-guide/_drafts/webr.qmd diff --git a/user-guide/_metadata.yml b/user-guide/_metadata.yml deleted file mode 100644 index 01dec92..0000000 --- a/user-guide/_metadata.yml +++ /dev/null @@ -1,9 +0,0 @@ -format: - html: - toc: true - toc-location: right - -knitr: - opts_chunk: - collapse: true - comment: "#>" \ No newline at end of file diff --git a/user-guide/complete-example.qmd b/user-guide/complete-example.qmd index 4fad36e..addaa50 100644 --- a/user-guide/complete-example.qmd +++ b/user-guide/complete-example.qmd @@ -1,40 +1,52 @@ --- title: "A Complete Example" subtitle: "A package from start to finish: Making a heckin' case converter." +execute: + eval: false --- -The Rust crate ecosystem is rich with very small and very powerful utility libraries. One of the most downloaded crates is [heck](https://docs.rs/heck). It provides traits and structs to perform some of the most common case conversions. - -In this tutorial we'll create a 0 dependency R package to provide the common case conversions. The resultant R package will be more performant but less flexible than the [`{snakecase}`](https://tazinho.github.io/snakecase/) R package. - -This tutorial covers: - -- vectorization -- `NA` handling -- code generation using a macro +The Rust crate ecosystem is rich with very small and very powerful utility +libraries. One of the most downloaded crates is [heck](https://docs.rs/heck). It +provides traits and structs to perform some of the most common case conversions. +In this tutorial we'll create a zero-dependency R package to provide the common +case conversions. This is mainly designed to give you a sense of the development +workflow and also showcase what can be done with extendr. The resulting R +package will be more performant but less flexible than the R package +[`{snakecase}`](https://tazinho.github.io/snakecase/). ## Getting started -Create a new R package: +The first thing we need to do is create a new R package: -```r +``` r usethis::create_package("heck") ``` -When the new R package has opened up, add `extendr`. +Once we have the basic R package setup, then we need to add the necessary +`extendr` scaffolding that will link our Rust code to R and ensure the package +builds properly. -```r +``` r rextendr::use_extendr(crate_name = "rheck", lib_name = "rheck") ``` ::: callout-note -When adding the extendr dependency, make sure that the `crate_name` and `lib_name` arguments _are not_ `heck`. In order to add the `heck` crate as a dependency, the crate itself cannot be called `heck` because it creates a recursive dependency. Doing this allows us to name the R package `{heck}`, but the internal Rust crate is called `rheck`. +Normally, the crate and library names will default to the R package name, but in +this case, we cannot do that because it would create a recursive dependency in +the Rust library. To get around this, we name the internal Rust crate and +library `rheck`, which allows us to name the R package `{heck}`. ::: -Next, `heck` is needed as a dependency. From your terminal, navigate to `src/rust` and run `cargo add heck`. With this, you have everything you need to get started. +Next, `heck` is needed as a dependency. The easiest way to do that is to call + +``` r +rextendr::use_crate("heck") +``` +If you prefer to use your terminal, however, simply navigate to `src/rust` and +run `cargo add heck`. With this, you have everything you need to get started. -## snek case conversion +## Scalar snek case conversion ```{r, include = FALSE} library(rextendr) @@ -45,7 +57,9 @@ knitr::opts_chunk$set(engine.opts = list(dependencies = list(heck = "0.5.0"))) use heck::ToSnekCase; ``` -Let's start by creating a simple function to take a single string, and convert it to snake case. First, the trait `ToSnekCase` needs to be imported so that the method `to_snek_case()` is available to `&str`. +Let's start by creating a simple function to take a single string, and convert +it to snake case. First, the trait `ToSnekCase` needs to be imported so that the +method `to_snek_case()` is available to `&str`. ```{extendrsrc} use heck::ToSnekCase; @@ -56,60 +70,71 @@ fn to_snek_case(x: &str) -> String { } ``` -Simple enough, right? Let's give it a shot. To make it accessible from your R session, it needs to be included in your `extendr_module! {}` macro. +Simple enough, right? Let's give it a shot. To make it accessible from your R +session, it needs to be included in your `extendr_module! {}` macro. -```rust +``` rust extendr_module! { mod heck; fn to_snek_case; } ``` -From your R session, run `rextendr::document()` followed by `devtools::load_all()` to make the function available. We'll skip these step from now on, but be sure to remember it! +From your R session, run `devtools::document()` followed by +`devtools::load_all()` to make the function available. ```{r} to_snek_case("MakeMe-Snake case") ``` -Rarely is it useful to run a function on just a scalar character value. Rust, though, works with scalars by default and adding vectorization is another step. +Rarely is it useful to run a function on just a scalar character value. Rust, +though, works with scalars by default and adding vectorization is another step. ```{r, error = TRUE} to_snek_case(c("DontStep", "on-Snek")) ``` -Providing a character vector causes an error. So how do you go about vectorizing? +Providing a character vector causes an error. So how do you go about +vectorizing? -## vectorizing snek case conversion +## Vectorizing snek case conversion -To vectorize this function, you need to be apply the conversion to each element in a character vector. The extendr wrapper struct for a character vector is called `Strings`. To take in a character vector and also return one, the function signature should look like this: +To vectorize this function, you need to apply the conversion to each element in +a character vector. The extendr wrapper struct for a character vector is called +`Strings`. To take in a character vector and also return one, the function +signature should look like this: -```rust +``` rust #[extendr] fn to_snek_case(x: Strings) -> Strings { } ``` -This says there is an argument `x` which must be a character vector and this function must also `->` return the `Strings` (a character vector). +This says there is an argument `x` which must be a character vector and this +function must also return the `Strings` (a character vector). The return type is +signaled by `->`. To iterate through this you can use the `.into_iter()` method +on the character vector. -To iterate through this you can use the `.into_iter()` method on the character vector. - -```rust +``` rust #[extendr] fn to_snek_case(x: Strings) -> Strings { - x - .into_iter() - // the rest of the function + x.into_iter() + // the rest of the function } ``` -Iterators have a method called `.map()` (yes, just like `purrr::map()`). It lets you apply a closure (an anonymous function) to each element of the iterator. In this case, each element is an [`Rstr`](https://extendr.github.io/extendr/extendr_api/wrapper/rstr/struct.Rstr.html). The `Rstr` has a method `.as_str()` which will return a string slice `&str`. You can take this slice and pass it on to `.to_snek_case()`. After having mapped over each element, the results are `.collect()`ed into another `Strings`. - +Iterators have a method called `.map()` (yes, just like `purrr::map()`). It lets +you apply a closure (an anonymous function) to each element of the iterator. In +this case, each element is an +[`Rstr`](https://extendr.github.io/extendr/extendr_api/wrapper/rstr/struct.Rstr.html). +The `Rstr` has a method `.as_str()` which will return a string slice `&str`. You +can take this slice and pass it on to `.to_snek_case()`. After having mapped +over each element, the results are `.collect()`ed into another `Strings`. ```{extendrsrc preamble = "use_heck"} #[extendr] fn to_snek_case(x: Strings) -> Strings { - x - .into_iter() + x.into_iter() .map(|xi| { xi.as_str().to_snek_case() }) @@ -117,27 +142,34 @@ fn to_snek_case(x: Strings) -> Strings { } ``` - -This new version of the function can be used in a vectorized manner: +This new version of the function can be used in a vectorized manner: ```{r} to_snek_case(c("DontStep", "on-Snek")) ``` -But can it handle a missing value out of the box? +But can it handle a missing value out of the box? ```{r} to_snek_case(c("DontStep", NA_character_, "on-Snek")) ``` -Well, sort of. The `as_str()` method when used on a missing value will return `"NA"` which is not in a user's best interest. - +Well, sort of. The `as_str()` method when used on a missing value will return +`"NA"` which is not in a user's best interest. -## handling missing values +## Handling missing values -Instead of returning `"na"`, it would be better to return an _actual_ missing value. Those can be created each scalar's `na()` method e.g. `Rstr::na()`. +Instead of returning `"na"`, it would be better to return an *actual* missing +value. Those can be created using each scalar's `na()` method e.g. `Rstr::na()`. -You can modify the `.map()` statement to check if an `NA` is present, and, if so, return an `NA` value. To perform this check, use the `is_na()` method which returns a `bool` which is either `true` or `false`. The result can be [`match`ed](https://doc.rust-lang.org/book/ch06-02-match.html). When it is missing, the match arm returns the `NA` scalar value. When it is not missing, the `Rstr` is converted to snek case. However, since the `true` arm is an `Rstr` the other `false` arm must _also_ be an `Rstr`. To accomplish this use the `Rstr::from()` method. +You can modify the `.map()` statement to check if an `NA` is present, and, if +so, return an `NA` value. To perform this check, use the `is_na()` method which +returns a `bool`, a value that is either `true` or `false`. The result can then +be [`match`ed](https://doc.rust-lang.org/book/ch06-02-match.html). When it is +missing, the match arm returns the `NA` scalar value. When it is not missing, +the `Rstr` is converted to snek case. However, since the `true` arm is an `Rstr` +the other `false` arm must *also* be an `Rstr`. To accomplish this use the +`Rstr::from()` method. ```{extendrsrc preamble = "use_heck", profile="release"} #[extendr] @@ -151,23 +183,27 @@ fn to_snek_case(x: Strings) -> Strings { } ``` -This function can now handle missing values! +This function can now handle missing values! ```{r} to_snek_case(c("DontStep", NA_character_, "on-Snek")) ``` -## automating other methods with a macro! - -There are traits for the other case conversions such as `ToKebabCase`, `ToPascalCase`, `ToShoutyKebabCase` and others. The each have a similar method name: `.to_kebab_case()`, `to_pascal_case()`, `.to_shouty_kebab_case()`. You can either choose to copy the above and change the method call multiple times, _or_ use a macro as a form of code generation. +## Automating code-writing with a macro! -A macro allows you to generate code in a short hand manner. This macro take an identifier which has a placeholder called `$fn_name`: `$fn_name:ident`. +There are traits for the other case conversions such as `ToKebabCase`, +`ToPascalCase`, `ToShoutyKebabCase` and others, with each having a similar +method name: `.to_kebab_case()`, `to_pascal_case()`, `.to_shouty_kebab_case()`. +You can either copy the above code for `to_snek_case()` and change the method +call multiple times, *or* you can use a macro as a form of code generation. A +macro allows you to generate code in a short hand manner. This macro takes an +identifier which has a placeholder called `$fn_name`: `$fn_name:ident`. -```rust +``` rust macro_rules! make_heck_fn { ($fn_name:ident) => { - #[extendr] /// @export + #[extendr] fn $fn_name(x: Strings) -> Strings { x.into_iter() .map(|xi| match xi.is_na() { @@ -180,7 +216,9 @@ macro_rules! make_heck_fn { } ``` -The `$fn_name` placeholder is put as the function name definition which is the same as the method name. To use this macro to generate the rest of the functions the other traits need to be imported. +The `$fn_name` placeholder is put as the function name definition which is the +same as the method name. To use this macro to generate the rest of the functions +the other traits need to be imported. ```{extendrsrc heck_traits} use heck::{ @@ -191,9 +229,10 @@ use heck::{ }; ``` -With the traits in scope, the macro can be invoked to generate the other functions. +With the traits in scope, the macro can be invoked to generate the other +functions. -```rust +``` rust make_heck_fn!(to_snek_case); make_heck_fn!(to_shouty_snake_case); make_heck_fn!(to_kebab_case); @@ -204,7 +243,8 @@ make_heck_fn!(to_train_case); make_heck_fn!(to_title_case); ``` -Note that each of these functions should be added to the `extendr_module! {}` macro in order for them to be available from R. +Note that each of these functions should be added to the `extendr_module! {}` +macro in order for them to be available from R. ```{extendrsrc preamble = "heck_traits", include = FALSE} #[extendr] @@ -218,21 +258,24 @@ fn to_shouty_kebab_case(x: Strings) -> Strings { } ``` -Test it out with the `to_shouty_kebab_case()` function! +Test it out with the `to_shouty_kebab_case()` function! ```{r} to_shouty_kebab_case("lorem:IpsumDolor__sit^amet") ``` -And with that, you've created an R package that provides case conversion using heck and with very little code! +And with that, you've created an R package that provides case conversion using +heck, and you've done it with just a few lines of code! +## Bench marking with `{snakecase}` -## bench marking with `{snakecase}` - -To illustrate the performance gains from using a vectorized Rust funciton, a `bench::mark()` is created between `to_snek_case()` and `snakecase::to_snake_case()`. +To illustrate the performance gains from using a vectorized Rust funciton, a +`bench::mark()` is created between `to_snek_case()` and +`snakecase::to_snake_case()`. ```{r include=FALSE} -rextendr::rust_source(code = r"( +rextendr::rust_source( + code = r"( use heck::ToSnekCase; #[extendr] @@ -244,10 +287,13 @@ fn to_snek_case(x: Strings) -> Strings { }) .collect::() } -)", dependencies = list("heck" = "*"), profile = "release") +)", + dependencies = list("heck" = "*"), + profile = "release" +) ``` -The bench mark will use 5000 randomly generated lorem ipsum sentences. +The bench mark will use 5000 randomly generated lorem ipsum sentences. ```{r, warning = FALSE} x <- unlist(lorem::ipsum(5000, 1, 25)) @@ -261,17 +307,18 @@ bench::mark( ``` ::: callout-note -The memory usage for rust-based functions is likely quite understated. This is because memory allocated outside of R cannot be tracked. - -See [bench::mark](https://bench.r-lib.org/reference/mark.html#value) documentation for more. +The memory usage for rust-based functions is likely quite understated. This is +because memory allocated outside of R cannot be tracked. +See [bench::mark](https://bench.r-lib.org/reference/mark.html#value) +documentation for more information. ::: - ## The whole thing -In just 42 lines of code (empty lines included), you can create a very performant R package! +In just 42 lines of code (empty lines included), you can create a very +performant R package! -```rust +``` rust use extendr_api::prelude::*; use heck::{ diff --git a/user-guide/index.qmd b/user-guide/index.qmd index 20096a9..f48f0ea 100644 --- a/user-guide/index.qmd +++ b/user-guide/index.qmd @@ -1,19 +1,16 @@ --- title: "User Guide" -subtitle: "A comprehensive guide to using extendr." -toc: false +subtitle: "A comprehensive guide to using extendr" +page-layout: article +toc: true +toc-depth: 0 +date: today +date-format: long --- -This user guide serves two primary functions: - -- First, and more generally, it explains how to call Rust code in R (and, as - it happens, how to call R code in Rust). -- Second, and more narrowly, it provides sufficient information and resources - to empower developers to build R packages with Rust extensions and publish - them to CRAN. - -Given these goals, the guide is primarily geared towards R users and R -developers who also have a decent grasp of Rust. +This user guide focuses on developing R packages that call Rust code and +publishing them to CRAN, so it is assumed that the reader has a basic +understanding of Rust. ## What's in this guide? @@ -21,15 +18,25 @@ We're working on that... ## What's *not* in this guide? -This book is not intended to be a comprehensive introduction to Rust. While do -go over some of the basics, more often than not, concepts are introduced only to -help explain extendr tools. That said, we do try to include copious references -and links to additional resources that the reader can follow to learn more about -specific topics. Of course, the biggest reference will be ["The -Book"](). +This book is not intended to be a comprehensive introduction to Rust. While we +do go over some of the basics, more often than not, concepts are introduced only +to help explain extendr tools. To learn more about Rust, you should consult +["The Book"](). This book is also not intended to be a comprehensive guide to R package development! There are, quite frankly, better resources for that, notably [R Packages (2e)](). As with Rust specifics, we will strive -to point you in the right direction to get more details and advice when you need -it. +to point you in the right direction to get more details and advice if you should +need it. + +## Software requirements + +This guide assumes the following versions of necessary software: + +- R: {{< var version.r >}} +- Rust {{< var version.rust >}} +- extendr-api {{< var version.extendr >}} +- rextendr {{< var version.rextendr >}} + +Please see [Get Started](get-started.qmd) if you have not already installed this +software. \ No newline at end of file diff --git a/user-guide/package-structure.qmd b/user-guide/package-structure.qmd new file mode 100644 index 0000000..002b146 --- /dev/null +++ b/user-guide/package-structure.qmd @@ -0,0 +1,200 @@ +--- +title: "Project Structure" +--- + +When you run `rextendr::use_extendr()` for the first time, it will generate the +scaffolding needed to integrate Rust code into an R package. The result is a +directory structure that is fairly unique among R packages, so this page serves +to walk you through all the generated files, explaining what they do. The +package directory should have the following structure: + +``` +. +├── R +│ └── extendr-wrappers.R +├── cleanup +├── cleanup.win +├── configure +├── configure.win +├── src +│ ├── .gitignore +│ ├── Makevars.in +│ ├── Makevars.win.in +│ ├── entrypoint.c +│ ├── -win.def +│ └── rust +│ ├── Cargo.toml +│ └── src +│ ├── document.rs +│ └── lib.rs +└── tools + ├── config.R + └── msrv.R +``` + +As an R package developer, the file `R/extendr-wrappers.R` will perhaps look the +most familiar to you. This file contains auto-generated R wrapper functions that +call compiled Rust code via `.Call()`. The file is regenerated every time you +call `devtools::document()`. It should never be edited by hand. Every other file +we describe below serves to ensure that those `.Call()` actually link to your +Rust source and that your R package compiles correctly. Developers of the +extendr project have carefully crafted these scaffolding files to ensure that R +package development is easy and robust. Very few of these should ever be edited +by hand. Below we use the {{< iconify openmoji:stop-sign >}} icon to signal that +the file should NOT be edited by hand. As a general rule, however, you should +only ever mess with `Cargo.toml` and scripts in `rust/src/`. + +::: callout-note +On this page, we only describe the content of files if it is helpful to you as +an R package developer. If you want to know more about how these files are +generated, you can have a look at the +[{rextendr} Internals](../contributing/rextendr-internals.qmd) page in the +Contributing guide. +::: + + +## Rust source files + +Everything associated with your Rust library crate will live in `src/rust`. Here +we will briefly describe some of the more important files contained there. + +### `~/Cargo.toml` + +The Rust package manifest defines the crate name, edition, dependencies, and +build profiles. If there are other crates you want to depend on, you will list +theme here. We provide a small utility `use_crate()` for this purpose. You +can also add them by hand or call `cargo add` in the terminal if you prefer. + +```{.toml filename="src/rust/Cargo.toml"} +[package] +name = 'hellorust' +publish = false +version = '0.1.0' +edition = '2021' +rust-version = '1.65' + +[lib] +crate-type = ['rlib', 'staticlib'] +name = 'hellorust' + +[[bin]] +name = 'document' +path = 'document.rs' + +[dependencies] +extendr-api = '*' + +[profile.release] +lto = true +codegen-units = 1 +``` + +Note that the crate is built as both an `rlib` (used by the `document` binary to +introspect exports) and a `staticlib` (linked into the R package). The `[[bin]]` +entry registers `document.rs` as an executable used to generate R wrappers at +build time. + +### `~/src/lib.rs` + +The main Rust library file where you gather Rust code to be linked into R. The +scaffolded version will initially contain a simple `hello_world()` example. + +```{.rust filename="src/rust/src/lib.rs"} +use extendr_api::prelude::*; + +/// Return string `"Hello world!"` to R. +/// @export +#[extendr] +fn hello_world() -> &'static str { + "Hello world!" +} + +// Macro to generate exports. +// This ensures exported functions are registered with R. +// See corresponding C code in `entrypoint.c`. +extendr_module! { + mod hellorust; + fn hello_world; +} +``` + +The `extendr_module!` macro controls which functions, `impl` blocks, and +submodules are exposed to R. Every function you want available in R must be +listed in an `extendr_module!`. + +### `~/document.rs` {{< iconify openmoji:stop-sign >}} + +A Rust binary that generates `R/extendr-wrappers.R` by introspecting the +exported function metadata at build time. This runs as part of the `Makevars` +build step via `cargo run --bin document`. You should never edit this file by +hand. + +## Build files + +### `src/entrypoint.c` {{< iconify openmoji:stop-sign >}} + +A small C file that forwards R's routine registration to the Rust library. This +is necessary to prevent the linker from stripping the static library. Here, too, +you should never edit this file by hand. + +### `src/Makevars.in` and `src/Makevars.win.in` {{< iconify openmoji:stop-sign >}} + +Makefile templates used to compile the Rust library as part of the R package +build process (whenever you call `R CMD INSTALL` or `devtools::install()`). These +are not used directly — `tools/config.R` reads them and writes the final +`src/Makevars` (or `src/Makevars.win`) at build time, interpolating placeholder +string variables that are specific to your package and operating system. As +noted above, the build step also runs `cargo run --bin document` to regenerate +`R/extendr-wrappers.R`. + +### `src/-win.def` {{< iconify openmoji:stop-sign >}} + +A Windows DLL export definitions file. It ensures the package's initialization +routine is exported correctly on Windows. + +## Configuration scripts + +These are tools that tailor the build process to the specific context of your +package and operating system. + +### `configure` and `configure.win` {{< iconify openmoji:stop-sign >}} + +Shell scripts run by R before compilation. They invoke `tools/config.R` via +`Rscript` to generate the final `Makevars` file from the `.in` template. As +their names suggest, `configure` is used on Unix and `configure.win` on Windows. + +### `tools/msrv.R` {{< iconify openmoji:stop-sign >}} + +Reads the `SystemRequirements` field from `DESCRIPTION` to determine the minimum +supported Rust version (MSRV), then verifies that the installed `rustc` and +`cargo` meet that requirement. An informative error is raised if they do not. + +### `tools/config.R` {{< iconify openmoji:stop-sign >}} + +Sources `tools/msrv.R`, then checks the `DEBUG` and `NOT_CRAN` environment +variables to determine the build profile. Reads `src/Makevars.in` (or `src/Makevars.win.in` +on Windows), substitutes all context-specific placeholder variables, and writes +the final `src/Makevars` (or `src/Makevars.win`). + +## Additional files + +### `src/.gitignore` + +This local .gitignore ignores artifacts of the build process that should not be +committed: + +```{.default filename="src/.gitignore"} +*.o +*.so +*.dll +target +.cargo +rust/vendor +Makevars +Makevars.win +``` + +### `cleanup` and `cleanup.win` {{< iconify openmoji:stop-sign >}} + +Shell scripts run by R when the package is uninstalled. They remove the +generated `src/Makevars` and `src/Makevars.win` files respectively. \ No newline at end of file diff --git a/user-guide/r-pkgs/package-structure.qmd b/user-guide/r-pkgs/package-structure.qmd deleted file mode 100644 index 94ed561..0000000 --- a/user-guide/r-pkgs/package-structure.qmd +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: "Project Structure" ---- - -A extendr-powered R package has a fairly unique structure. This section briefly outlines the structure of an extendr package and the important files. - -extendr works by creating a Rust library crate in `src/rust` that is defined by `src/rust/Cargo.toml`. - -```{.toml filename="Cargo.toml"} -[package] -name = 'hellorust' -publish = false -version = '0.1.0' -edition = '2021' - -[lib] -crate-type = [ 'staticlib' ] -name = 'hellorust' - -[dependencies] -extendr-api = '*' -``` - -Note the `crate-type = [ 'staticlib' ]`. When this library is compiled, it creates a static library which can then be called from R. - - -## Controlling exports to R: `lib.rs` - -The `lib.rs` file determines what will be exposed to your R package. The `extendr_module!` macro in `lib.rs` controls what will have wrappers provided to your R package. - -```toml -extendr_module! { - mod hellorust; - fn hello_world; -} -``` - -The `mod hellorust` is the name of the R package. Additional functions, `impl`s, and modules can also be added to this macro. - -## Building the package: `Makevars` - -When creating an R package that uses compiled code, a file called `Makevars` is used. - -::: callout-note -See [Using Makevars](https://cran.r-project.org/doc/manuals/R-exts.html#Using-Makevars) in Writing R Extensions for a thorough discussion. -::: - -`Makevars` is used as a preprocessing step for compiling an R package. The files `Makevars` and `Makevars.win` compile the Rust library in `src/rust`, and link to the library. - -::: callout-tip -`Makevars` is used for *nix operating systems and `Makevars.win` is used for Windows. -::: \ No newline at end of file diff --git a/user-guide/type-mapping/extendr-macro.qmd b/user-guide/type-mapping/extendr-macro.qmd deleted file mode 100644 index 8e51b22..0000000 --- a/user-guide/type-mapping/extendr-macro.qmd +++ /dev/null @@ -1,401 +0,0 @@ ---- -title: "The extendr Macro" ---- - -The power of extendr is in its ability to use Rust from R. The `#[extendr]` macro is what determines what is exported to R from Rust. This section covers the basic usage of the `#[extendr]` macro. - -[`#[extendr]`](https://extendr.github.io/extendr/extendr_api/attr.extendr.html) is what is referred to as an [attribute macro](https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros) (which itself is a type of [procedural macro](https://doc.rust-lang.org/reference/procedural-macros.html)). An attribute macro is attached to an [item](https://doc.rust-lang.org/reference/items.html) such as a function, `struct`, `enum`, or `impl`. - -The `#[extendr]` attribute macro indicates that an item should be made available to R. However, it _can only be used_ with a function or an impl block. - -```{r, include = FALSE} -library(rextendr) -``` - -## Exporting functions - -In order to make a function available to R, two things must happen. First, the `#[extendr]` macro must be attached to the function. For example, you can create a function `answer_to_life()` - -::: {.callout-note collapse="true"} -In the Hitchhiker's Guide to the Galaxy, the number 42 is the answer to the universe. See this fun [article from Scientific American](https://www.scientificamerican.com/article/for-math-fans-a-hitchhikers-guide-to-the-number-42/) -::: - -```rust -#[extendr] -fn answer_to_life() -> i32 { - 42 -} -``` - -By adding the `#[extendr]` attribute macro to the `answer_to_life()` function, we are indicating that this function has to be compatible with R. This alone, however, does not make the function available to R. It must be made available via the `extendr_module! {}` macro in `lib.rs`. - -```rust -extendr_module! { - mod hellorust; - fn answer_to_life; -} -``` - -::: callout-tip -Everything that is made available in the `extendr_module! {}` macro in `lib.rs` must be compatible with R as indicated by the `#[extendr]` macro. Note that the module name `mod hellorust` must be the name of the R package that this is part of. If you have created your package with `rextendr::use_extendr()` this should be set automatically. See [Hello, world!](../r-pkgs/package-setup.qmd). -::: - -What happens if you try and return something that cannot be represented by R? Take this example, an enum `Shape` is defined and a function takes a string `&str`. Based on the value of the arugment, an enum variant is returned. - -```rust -#[derive(Debug)] -enum Shape { - Triangle, - Rectangle, - Pentagon, - Hexagon, -} - -#[extendr] -fn make_shape(shape: &str) -> Shape { - match shape { - "triangle" => Shape::Triangle, - "rectangle" => Shape::Rectangle, - "pentagon" => Shape::Pentagon, - "hexagon" => Shape::Hexagon, - &_ => unimplemented!() - } -} -``` - -When this is compiled, an error occurs because extendr does not know how to convert the `Shape` enum into something that R can use. The error is fairly informative! - -```rust - | ^^^^^^^^^^ the trait `ToVectorValue` is not implemented for `Shape`, which is required by `extendr_api::Robj: From` - | - = help: the following other types implement trait `ToVectorValue`: - bool - i8 - i16 - i32 - i64 - usize - u8 - u16 - and 45 others - = note: required for `extendr_api::Robj` to implement `From` - = note: this error originates in the attribute macro `extendr` -``` - -It tells you that `Shape` does not implement the `ToVectorValue` trait. The `ToVectorValue` trait is what enables items from Rust to be returned to R. - -## `ToVectorValue` trait - -In order for an item to be returned from a function marked with the `#[extendr]` attribute macro, it must be able to be turned into an R object. In extendr, the struct `Robj` is a catch all for any type of R object. - -::: callout-note -For those familiar with PyO3, the `Robj` struct is similar in concept to the [`PyAny`](https://docs.rs/pyo3/latest/pyo3/types/struct.PyAny.html) struct. -::: - -The `ToVectorValue` trait is what is used to convert Rust items into R objects. The trait is implemented on a number of standard Rust types such as `i32`, `f64`, `usize`, `String` and more (see [all foreign implementations here](https://extendr.github.io/extendr/extendr_api/robj/into_robj/trait.ToVectorValue.html#foreign-impls)) which enables these functions to be returned from a Rust function marked with `#[extendr]`. - -::: callout-note -In essence, all items that are returned from a function must be able to be turned into an `Robj`. Other extendr types such as `List`, for example, have a `From for Robj` implementation that defines how it is converted into an `Robj`. -::: - -This means that with a little extra work, the `Shape` enum can be returned to R. To do so, the `#[extendr]` macro needs to be added to an impl block. - - -## Exporting `impl` blocks - -The other supported item that can be made available to R is an [`impl`](https://doc.rust-lang.org/std/keyword.impl.html) block. -`impl` is a keyword that allows you to _implement_ a trait or an inherent implementation. The `#[extendr]` macro works with inherent implementations. These are `impl`s on a type such as an `enum` or a `struct`. extendr _does not_ support using `#[extendr]` on trait impls. - -::: callout-note -You can only add an inherent implementation on a type that you have own and not provided by a third party crate. This would violate the [orphan rules](https://github.com/Ixrec/rust-orphan-rules?tab=readme-ov-file#what-are-the-orphan-rules). -::: - -Continuing with the `Shape` example, this enum alone cannot be returned to R. For example, the following code will result in a compilation error - -```rust -#[derive(Debug)] -enum Shape { - Triangle, - Rectangle, - Pentagon, - Hexagon, -} - -#[extendr] -fn make_shape(shape: &str) -> Shape { - match shape { - "triangle" => Shape::Triangle, - "rectangle" => Shape::Rectangle, - "pentagon" => Shape::Pentagon, - "hexagon" => Shape::Hexagon, - &_ => unimplemented!() - } -} -``` -``` -error[E0277]: the trait bound `Shape: ToVectorValue` is not satisfied - --> src/lib.rs:19:1 - | -19 | #[extendr] - | ^^^^^^^^^^ the trait `ToVectorValue` is not implemented for `Shape`, which is required by `extendr_api::Robj: From` - | -``` - -However, you add the `#[extendr]` attribute to the `Shape` enum, it can be returned to R. - -```{extendrsrc} -#[derive(Debug)] -#[extendr] -enum Shape { - Triangle, - Rectangle, - Pentagon, - Hexagon, -} - -#[extendr] -fn make_shape(shape: &str) -> Shape { - match shape { - "triangle" => Shape::Triangle, - "rectangle" => Shape::Rectangle, - "pentagon" => Shape::Pentagon, - "hexagon" => Shape::Hexagon, - &_ => unimplemented!() - } -} - -``` - -It is also possible to add methods to structs/enums and their instances: - -```{extendrsrc} -#[derive(Debug)] -#[extendr] -enum Shape { - Triangle, - Rectangle, - Pentagon, - Hexagon, -} - -#[extendr] -impl Shape { - fn new(x: &str) -> Self { - match x { - "triangle" => Self::Triangle, - "rectangle" => Self::Rectangle, - "pentagon" => Self::Pentagon, - "hexagon" => Self::Hexagon, - &_ => unimplemented!(), - } - } - - fn n_coords(&self) -> usize { - match &self { - Shape::Triangle => 3, - Shape::Rectangle => 4, - Shape::Pentagon => 4, - Shape::Hexagon => 5, - } - } -} -``` - -In this example two new methods are added to the `Shape` enum. The first `new()` is like the `make_shape()` function that was shown earlier: it takes a `&str` and returns an enum variant. Now that the enum has an `impl` block with `#[extendr]` attribute macro, it can be exported to R by inclusion in the `extendr_module! {}` macro. - -```rust -extendr_module! { - mod hellorust; - impl Shape; -} -``` - -Doing so creates an environment in your package called `Shape`. The environment contains all of the methods that are available to you. - -::: callout-tip -There are use cases where you may not want to expose any methods but do want to make it possible to return a struct or an enum to the R. You can do this by adding an empty impl block with the `#[extendr]` attribute macro. -::: - -If you run `as.list(Shape)` you will see that there are two functions in the environment which enable you to call the methods defined in the impl block. You might think that this feel like an [R6 object](https://r6.r-lib.org/articles/Introduction.html) and you'd be right because an R6 object essentially is an environment! - -```{r} -as.list(Shape) -``` - -Calling the `new()` method instantiates a new enum variant. - -```{r} -tri <- Shape$new("triangle") -tri -``` - -The newly made `tri` object is an [external pointer](https://cran.r-project.org/doc/manuals/R-exts.html#External-pointers-and-weak-references) to the `Shape` enum in Rust. This pointer has the same methods as the Shape environment—though they cannot be seen in the same way. For example you can run the `n_coords()` method on the newly created object. - -```{r} -tri$n_coords() -``` - -::: callout-tip -To make the methods visible to the `Shape` class you can define a `.DollarNames` method which will allow you to preview the methods and attributes when using the `$` syntax. This is very handy to define when making an impl a core part of your package. - -```{r} -.DollarNames.Shape = function(env, pattern = "") { - ls(Shape, pattern = pattern) -} -``` - -::: - -### `impl` ownership - -Adding the `#[extendr]` macro to an impl allows the struct or enum to be made available to R as an external pointer. Once you create an external pointer, that is then owned by R. So you can only get references to it or mutable references. If you need an owned version of the type, then you will need to clone it. - -## Accessing exported `impl`s from Rust - -Invariably, if you have made an impl available to R via the `#[extendr]` macro, you may want to define functions that take the impl as a function argument. - -Due to R owning the `impl`'s external pointer, these functions cannot take an owned version of the impl as an input. For example trying to define a function that subtracts an integer from the `n_coords()` output like below returns a compiler error. - -```rust -#[extendr] -fn subtract_coord(x: Shape, n: i32) -> i32 { - (x.n_coords() as i32) - n -} -``` -``` -the trait bound `Shape: extendr_api::FromRobj<'_>` is not satisfied - --> src/lib.rs:53:22 - | - | fn subtract_coord(x: Shape, n: i32) -> i32 { - | ^^^^^ the trait `extendr_api::FromRobj<'_>` is not implemented for `Shape` - | -help: consider borrowing here - | - | fn subtract_coord(x: &Shape, n: i32) -> i32 { - | + - | fn subtract_coord(x: &mut Shape, n: i32) -> i32 { - | ++++ -``` - -As most often, the compiler's suggestion is a good one. Use `&Shape` to use a reference. - -## `ExternalPtr`: returning arbitrary Rust types - -In the event that you need to return a Rust type to R that doesn't have a compatible impl or is a type that you don't own, you can use `ExternalPtr`. The `ExternalPtr` struct allows any item to be captured as a pointer and returned to R. - -Here, for example, an `ExternalPtr` is returned from the `shape_ptr()` function. - -::: callout-tip -Anything that is wrapped in `ExternalPtr` must implement the `Debug` trait. -::: - -```{extendrsrc} -#[derive(Debug)] -enum Shape { - Triangle, - Rectangle, - Pentagon, - Hexagon, -} - -#[extendr] -fn shape_ptr(shape: &str) -> ExternalPtr { - let variant = match shape { - "triangle" => Shape::Triangle, - "rectangle" => Shape::Rectangle, - "pentagon" => Shape::Pentagon, - "hexagon" => Shape::Hexagon, - &_ => unimplemented!(), - }; - - ExternalPtr::new(variant) -} -``` - -Using an external pointer, however, is far more limiting than the `impl` block. For example, you cannot access any of its methods. - -```{r, error = TRUE} -tri_ptr <- shape_ptr("triangle") -tri_ptr$n_coords() -``` - -To use an `ExternalPtr`, you have to go through a bit of extra work for it. - -```{extendrsrc, include = FALSE} -#[derive(Debug)] -enum Shape { - Triangle, - Rectangle, - Pentagon, - Hexagon, -} - -#[extendr] -impl Shape { - fn new(x: &str) -> Self { - match x { - "triangle" => Self::Triangle, - "rectangle" => Self::Rectangle, - "pentagon" => Self::Pentagon, - "hexagon" => Self::Hexagon, - &_ => unimplemented!(), - } - } - - fn n_coords(&self) -> usize { - match &self { - Shape::Triangle => 3, - Shape::Rectangle => 4, - Shape::Pentagon => 4, - Shape::Hexagon => 5, - } - } -} - -#[extendr] -fn shape_ptr(shape: &str) -> ExternalPtr { - let variant = match shape { - "triangle" => Shape::Triangle, - "rectangle" => Shape::Rectangle, - "pentagon" => Shape::Pentagon, - "hexagon" => Shape::Hexagon, - &_ => unimplemented!(), - }; - - ExternalPtr::new(variant) -} - -#[extendr] -fn n_coords_ptr(x: Robj) -> i32 { - let shape = TryInto::>::try_into(x); - - match shape { - Ok(shp) => shp.n_coords() as i32, - Err(_) => 0 - } -} -``` - -```rust -#[extendr] -fn n_coords_ptr(x: Robj) -> i32 { - let shape = TryInto::>::try_into(x); - - match shape { - Ok(shp) => shp.n_coords() as i32, - Err(_) => 0 - } -} -``` - -This function definition takes an `Robj` and from it, tries to create an `ExternalPtr`. Then, if the conversion did not error, it returns the number of coordinates as an `i32` (R's version of an integer) and if there was an error converting, it returns 0. - -```{r} -tri_ptr <- shape_ptr("triangle") - -n_coords_ptr(tri_ptr) - -n_coords_ptr(list()) -``` - -For a good example of using `ExternalPtr` within an R package, refer to the [`b64` R package](https://github.com/extendr/b64).