├── .github ├── .gitignore └── workflows │ ├── pkgdown-build │ └── action.yml │ ├── roxygenize │ └── action.yml │ ├── git-identity │ └── action.yml │ ├── pkgdown-deploy │ └── action.yml │ ├── dep-suggests-matrix │ ├── action.yml │ └── action.R │ ├── rate-limit │ └── action.yml │ ├── versions-matrix │ ├── action.yml │ └── action.R │ ├── get-extra │ └── action.yml │ ├── matrix-check │ └── action.yml │ ├── lock.yaml │ ├── check │ └── action.yml │ ├── repo-state │ └── action.yml │ ├── style │ └── action.yml │ ├── covr │ └── action.yml │ ├── pkgdown.yaml │ ├── format-suggest.yaml │ ├── copilot-setup-steps.yaml │ ├── R-CMD-check-status.yaml │ ├── commit-suggest.yaml │ ├── pr-commands.yaml │ ├── update-snapshots │ └── action.yml │ ├── R-CMD-check-dev.yaml │ ├── dep-matrix │ └── action.yml │ ├── commit │ └── action.yml │ ├── install │ └── action.yml │ └── revdep.yaml ├── vignettes ├── .gitignore └── typed-in-packages.Rmd ├── docs ├── logo.png ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── apple-touch-icon-120x120.png ├── apple-touch-icon-152x152.png ├── apple-touch-icon-180x180.png ├── apple-touch-icon-60x60.png ├── apple-touch-icon-76x76.png ├── reference │ ├── figures │ │ └── logo.png │ ├── get_assertion.html │ ├── as_assertion_factory.html │ ├── process_assertion_factory_dots.html │ ├── index.html │ └── static_typing.html ├── pkgdown.yml ├── link.svg ├── bootstrap-toc.css ├── docsearch.js ├── pkgdown.js ├── bootstrap-toc.js ├── 404.html ├── authors.html └── pkgdown.css ├── .gitignore ├── man ├── figures │ └── logo.png ├── as_assertion_factory.Rd ├── use_typed.Rd ├── process_assertion_factory_dots.Rd ├── typed-package.Rd ├── check_arg.Rd ├── declare.Rd └── assertion_factories.Rd ├── _pkgdown.yml ├── pkgdown └── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ └── apple-touch-icon-180x180.png ├── .travis.yml ├── R ├── typed-package.R ├── 04_print.R ├── zzz.R ├── 07_use_typed.R ├── 05_new_type.R └── 02_declare.R ├── .Rbuildignore ├── tests ├── testthat │ ├── test-47.R │ ├── test-others.R │ ├── test-declare.R │ ├── test-qm.R │ └── test-native_types.R └── testthat.R ├── codecov.yml ├── typed.Rproj ├── NAMESPACE ├── DESCRIPTION ├── cran-comments.md ├── README.Rmd └── README.md /.github/.gitignore: -------------------------------------------------------------------------------- 1 | /pkg.lock 2 | *.html 3 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/docs/logo.png -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | docs 5 | inst/doc 6 | /doc/ 7 | /Meta/ 8 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/man/figures/logo.png -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://moodymudskipper.github.io/typed/ 2 | template: 3 | bootstrap: 5 4 | 5 | -------------------------------------------------------------------------------- /docs/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/docs/favicon-16x16.png -------------------------------------------------------------------------------- /docs/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/docs/favicon-32x32.png -------------------------------------------------------------------------------- /docs/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/docs/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /docs/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/docs/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /docs/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/docs/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /docs/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/docs/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /docs/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/docs/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /docs/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/docs/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /docs/reference/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/docs/reference/figures/logo.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/typed/HEAD/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # R for travis: see documentation at https://docs.travis-ci.com/user/languages/r 2 | 3 | language: R 4 | cache: packages 5 | after_success: 6 | - Rscript -e 'covr::codecov()' 7 | -------------------------------------------------------------------------------- /R/typed-package.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | "_PACKAGE" 3 | 4 | ## usethis namespace: start 5 | #' @importFrom utils getFromNamespace assignInNamespace 6 | ## usethis namespace: end 7 | NULL 8 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^typed\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^README\.Rmd$ 4 | ^\.travis\.yml$ 5 | ^codecov\.yml$ 6 | ^_pkgdown\.yml$ 7 | ^docs$ 8 | ^pkgdown$ 9 | ^cran-comments\.md$ 10 | ^CRAN-RELEASE$ 11 | ^\.github$ 12 | ^doc$ 13 | ^Meta$ 14 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown-build/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to build a pkgdown website" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Build site 7 | run: | 8 | pkgdown::build_site() 9 | shell: Rscript {0} 10 | -------------------------------------------------------------------------------- /tests/testthat/test-47.R: -------------------------------------------------------------------------------- 1 | test_that("#47", { 2 | f <- typed::Integer() ? function() { 3 | typed::Double() ? foo <- local({ 4 | return(as.double(1)) 5 | }) 6 | return(as.integer(1)) 7 | } 8 | 9 | expect_error(f(), NA) 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/roxygenize/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to create documentation with roxygen2" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Roxygenize 7 | run: | 8 | try(roxygen2::roxygenize()) 9 | shell: Rscript {0} 10 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /docs/pkgdown.yml: -------------------------------------------------------------------------------- 1 | pandoc: 3.1.11 2 | pkgdown: 2.0.9 3 | pkgdown_sha: ~ 4 | articles: 5 | typed-in-packages: typed-in-packages.html 6 | last_built: 2025-01-03T16:27Z 7 | urls: 8 | reference: https://moodymudskipper.github.io/typed/reference 9 | article: https://moodymudskipper.github.io/typed/articles 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/git-identity/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to set up a Git identity" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Configure Git identity 7 | run: | 8 | env | sort 9 | git config --local user.name "$GITHUB_ACTOR" 10 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 11 | shell: bash 12 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown-deploy/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to deploy a pkgdown website" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Deploy site 7 | uses: nick-fields/retry@v3 8 | with: 9 | timeout_minutes: 15 10 | max_attempts: 10 11 | command: | 12 | R -q -e 'pkgdown::deploy_to_branch(new_process = FALSE)' 13 | -------------------------------------------------------------------------------- /.github/workflows/dep-suggests-matrix/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to compute a matrix with all suggested packages" 2 | outputs: 3 | matrix: 4 | description: "Generated matrix" 5 | value: ${{ steps.set-matrix.outputs.matrix }} 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - id: set-matrix 11 | run: | 12 | Rscript ./.github/workflows/dep-suggests-matrix/action.R 13 | shell: bash 14 | -------------------------------------------------------------------------------- /.github/workflows/rate-limit/action.yml: -------------------------------------------------------------------------------- 1 | name: "Check GitHub rate limits" 2 | inputs: 3 | token: # id of input 4 | description: GitHub token, pass secrets.GITHUB_TOKEN 5 | required: true 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Check rate limits 11 | run: | 12 | curl -s --header "authorization: Bearer ${{ inputs.token }}" https://api.github.com/rate_limit 13 | shell: bash 14 | -------------------------------------------------------------------------------- /man/as_assertion_factory.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/05_new_type.R 3 | \name{as_assertion_factory} 4 | \alias{as_assertion_factory} 5 | \title{Build a new type} 6 | \usage{ 7 | as_assertion_factory(f) 8 | } 9 | \arguments{ 10 | \item{f}{a function} 11 | } 12 | \value{ 13 | a function with class \code{assertion_factory} 14 | } 15 | \description{ 16 | Build a new type 17 | } 18 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/testing-design.html#sec-tests-files-overview 7 | # * https://testthat.r-lib.org/articles/special-files.html 8 | 9 | library(testthat) 10 | library(typed) 11 | 12 | test_check("typed") 13 | -------------------------------------------------------------------------------- /man/use_typed.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/07_use_typed.R 3 | \name{use_typed} 4 | \alias{use_typed} 5 | \title{Use the 'typed' package} 6 | \usage{ 7 | use_typed() 8 | } 9 | \value{ 10 | Returns \code{NULL} invisibly, called for side effects 11 | } 12 | \description{ 13 | This sets up your package so you can use 'typed', by editing the DESCRIPTION file 14 | and editing or creating 'R/your.pkg-package.R' 15 | } 16 | -------------------------------------------------------------------------------- /man/process_assertion_factory_dots.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/05_new_type.R 3 | \name{process_assertion_factory_dots} 4 | \alias{process_assertion_factory_dots} 5 | \title{Process assertion factory dots} 6 | \usage{ 7 | process_assertion_factory_dots(...) 8 | } 9 | \arguments{ 10 | \item{...}{dots} 11 | } 12 | \value{ 13 | a \verb{\{} expression 14 | } 15 | \description{ 16 | This needs to be exported, but shouldn't be called by the user 17 | } 18 | -------------------------------------------------------------------------------- /typed.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /.github/workflows/versions-matrix/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to compute a matrix with all R and OS versions" 2 | 3 | outputs: 4 | matrix: 5 | description: "Generated matrix" 6 | value: ${{ steps.set-matrix.outputs.matrix }} 7 | 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Install json2yaml 12 | run: | 13 | sudo npm install -g json2yaml 14 | shell: bash 15 | 16 | - id: set-matrix 17 | run: | 18 | Rscript ./.github/workflows/versions-matrix/action.R 19 | shell: bash 20 | -------------------------------------------------------------------------------- /.github/workflows/get-extra/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to determine extra packages to be installed" 2 | outputs: 3 | packages: 4 | description: "List of extra packages" 5 | value: ${{ steps.get-extra.outputs.packages }} 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Get extra packages 11 | id: get-extra 12 | run: | 13 | set -x 14 | packages=$( ( grep Config/gha/extra-packages DESCRIPTION || true ) | cut -d " " -f 2) 15 | echo packages=$packages >> $GITHUB_OUTPUT 16 | shell: bash 17 | -------------------------------------------------------------------------------- /R/04_print.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | print.typed <- function(x, ...) { 3 | x2 <- x 4 | attributes(x2) <- NULL 5 | writeLines(text = "# typed function") 6 | print(x2) 7 | if(!identical(attr(x, "return_type"), NA)) { 8 | writeLines(text = paste0( 9 | "# Return type: ", deparse1(attr(x ,"return_type")))) 10 | } 11 | if(length(attr(x, "arg_types"))) { 12 | writeLines(text = "# Arg types:") 13 | for(arg in names(attr(x, "arg_types"))) { 14 | writeLines(text = paste0("# ", 15 | arg, ": ", deparse1(attr(x ,"arg_types")[[arg]]))) 16 | } 17 | } 18 | invisible(x) 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/matrix-check/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to check a matrix with all R and OS versions, computed with the versions-matrix action" 2 | inputs: 3 | matrix: 4 | description: "Generated matrix" 5 | required: true 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Install json2yaml 11 | run: | 12 | sudo npm install -g json2yaml 13 | shell: bash 14 | 15 | - run: | 16 | matrix='${{ inputs.matrix }}' 17 | if [ -n "${matrix}" ]; then 18 | echo $matrix | jq . 19 | echo $matrix | json2yaml 20 | else 21 | echo "No matrix found" 22 | fi 23 | shell: bash 24 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | # deal with the conflicting `?` in the devtools_shims env 2 | # follow up in https://github.com/r-lib/pkgload/issues/265 3 | 4 | .onAttach <- function(...) { 5 | if (isNamespaceLoaded("pkgload")) { 6 | attr(`?`, "original") <- getFromNamespace("shim_question", "pkgload") 7 | getFromNamespace("assignInNamespace", "utils")("shim_question", `?`, "pkgload") 8 | } 9 | } 10 | 11 | .onDetach <- function(...) { 12 | if (isNamespaceLoaded("pkgload")) { 13 | original <- attr(getFromNamespace("shim_question", "pkgload"), "original") 14 | if (!is.null(original)) { 15 | getFromNamespace("assignInNamespace", "utils")(original, `?`, "pkgload") 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/lock.yaml: -------------------------------------------------------------------------------- 1 | name: "Lock threads" 2 | permissions: 3 | issues: write 4 | pull-requests: write 5 | discussions: write 6 | on: 7 | workflow_dispatch: 8 | schedule: 9 | - cron: "37 2 * * *" 10 | 11 | jobs: 12 | lock: 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - uses: krlmlr/lock-threads@patch-1 16 | with: 17 | github-token: ${{ github.token }} 18 | issue-inactive-days: "365" 19 | issue-lock-reason: "" 20 | issue-comment: > 21 | This old thread has been automatically locked. If you think you have 22 | found something related to this, please open a new issue and link to this 23 | old issue if necessary. 24 | -------------------------------------------------------------------------------- /R/07_use_typed.R: -------------------------------------------------------------------------------- 1 | #' Use the 'typed' package 2 | #' 3 | #' This sets up your package so you can use 'typed', by editing the DESCRIPTION file 4 | #' and editing or creating 'R/your.pkg-package.R' 5 | #' 6 | #' @return Returns `NULL` invisibly, called for side effects 7 | #' @export 8 | use_typed <- function() { 9 | rlang::check_installed("usethis") 10 | rlang::check_installed("desc") 11 | 12 | usethis::use_package("typed") 13 | pkg_doc_path <- sprintf( 14 | "R/%s-package.R", 15 | desc::desc(usethis::proj_get())$get_field("Package") 16 | ) 17 | if (!file.exists(pkg_doc_path)) usethis::use_package_doc(open = FALSE) 18 | usethis::use_import_from("typed", getNamespaceExports("typed"), load = FALSE) 19 | invisible(NULL) 20 | } 21 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,typed) 4 | export("?") 5 | export(Any) 6 | export(Array) 7 | export(Builtin) 8 | export(Character) 9 | export(Closure) 10 | export(Data.frame) 11 | export(Date) 12 | export(Dots) 13 | export(Double) 14 | export(Environment) 15 | export(Expression) 16 | export(Factor) 17 | export(Function) 18 | export(Integer) 19 | export(Language) 20 | export(List) 21 | export(Logical) 22 | export(Matrix) 23 | export(Null) 24 | export(Pairlist) 25 | export(Raw) 26 | export(Special) 27 | export(Symbol) 28 | export(Time) 29 | export(as_assertion_factory) 30 | export(check_arg) 31 | export(check_output) 32 | export(declare) 33 | export(process_assertion_factory_dots) 34 | export(use_typed) 35 | importFrom(utils,assignInNamespace) 36 | importFrom(utils,getFromNamespace) 37 | importFrom(utils,help) 38 | -------------------------------------------------------------------------------- /docs/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /tests/testthat/test-others.R: -------------------------------------------------------------------------------- 1 | test_that("as_assertion_factory works", { 2 | Numeric <- as_assertion_factory(function(value) { 3 | if(!is.numeric(value)) { 4 | stop("!!!", call. = FALSE) 5 | } 6 | value 7 | }) 8 | expect_equal(Numeric()(1), 1) 9 | expect_error(Numeric()("a")) 10 | expect_equal(Numeric(anyNA = FALSE)(1), 1) 11 | expect_error(Numeric(anyNA = TRUE)(1)) 12 | expect_equal(Numeric(~.==1)(1), 1) 13 | expect_error(Numeric(~.==1)("a")) 14 | expect_equal(Numeric("!!!" ~ . == 1)(1), 1) 15 | expect_error(Numeric("!!!" ~ . == 1)("a")) 16 | expect_error(Numeric("foo")(1)) 17 | 18 | declare("x", Double()) 19 | x <- 1 20 | get_assertion(x) 21 | expect_error(get_assertion(x), NA) 22 | }) 23 | 24 | 25 | test_that("printing works", { 26 | fun1 <- ? function() {} 27 | expect_equal(print(fun1), fun1) 28 | fun2 <- Double() ? function(x = ? Double()) {} 29 | expect_equal(print(fun2), fun2) 30 | }) 31 | -------------------------------------------------------------------------------- /.github/workflows/check/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to check an R package" 2 | inputs: 3 | results: 4 | description: Slug for check results 5 | required: true 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - uses: r-lib/actions/check-r-package@v2 11 | with: 12 | # Fails on R 3.6 on Windows, remove when this job is removed? 13 | args: 'c("--no-manual", "--as-cran", "--no-multiarch")' 14 | error-on: ${{ env.RCMDCHECK_ERROR_ON || '"note"' }} 15 | 16 | - name: Show test output 17 | if: always() 18 | run: | 19 | ## -- Show test output -- 20 | echo "::group::Test output" 21 | find check -name '*.Rout*' -exec head -n 1000000 '{}' \; || true 22 | echo "::endgroup::" 23 | shell: bash 24 | 25 | - name: Upload check results 26 | if: failure() 27 | uses: actions/upload-artifact@main 28 | with: 29 | name: ${{ inputs.results }}-results 30 | path: check 31 | -------------------------------------------------------------------------------- /man/typed-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/typed-package.R 3 | \docType{package} 4 | \name{typed-package} 5 | \alias{typed} 6 | \alias{typed-package} 7 | \title{typed: Support Types for Variables, Arguments, and Return Values} 8 | \description{ 9 | \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} 10 | 11 | A type system for R. It supports setting variable types in a script or the body of a function, so variables can't be assigned illegal values. Moreover it supports setting argument and return types for functions. 12 | } 13 | \seealso{ 14 | Useful links: 15 | \itemize{ 16 | \item \url{https://github.com/moodymudskipper/typed} 17 | \item \url{https://moodymudskipper.github.io/typed/} 18 | \item Report bugs at \url{https://github.com/moodymudskipper/typed/issues} 19 | } 20 | 21 | } 22 | \author{ 23 | \strong{Maintainer}: Antoine Fabri \email{antoine.fabri@gmail.com} 24 | 25 | } 26 | \keyword{internal} 27 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: typed 2 | Title: Support Types for Variables, Arguments, and Return Values 3 | Version: 0.0.1.9000 4 | Authors@R: 5 | person(given = "Antoine", 6 | family = "Fabri", 7 | role = c("aut", "cre"), 8 | email = "antoine.fabri@gmail.com") 9 | Description: A type system for R. It supports setting variable types in a script or the body of a function, so variables can't be assigned illegal values. Moreover it supports setting argument and return types for functions. 10 | License: GPL-3 11 | Encoding: UTF-8 12 | Language: en 13 | LazyData: true 14 | Roxygen: list(markdown = TRUE) 15 | RoxygenNote: 7.3.3.9000 16 | URL: https://github.com/moodymudskipper/typed, https://moodymudskipper.github.io/typed/ 17 | BugReports: https://github.com/moodymudskipper/typed/issues 18 | Imports: 19 | utils, 20 | waldo 21 | Suggests: 22 | testthat (>= 3.0.0), 23 | covr, 24 | knitr, 25 | rmarkdown, 26 | desc, 27 | ggplot2, 28 | rlang, 29 | usethis, 30 | vctrs 31 | Config/testthat/edition: 3 32 | VignetteBuilder: knitr 33 | -------------------------------------------------------------------------------- /.github/workflows/repo-state/action.yml: -------------------------------------------------------------------------------- 1 | name: "Determine repository state" 2 | description: "Expose protected, foreign, and is_pr context variables" 3 | outputs: 4 | protected: 5 | description: "Whether the current branch is protected" 6 | value: ${{ steps.state.outputs.protected }} 7 | foreign: 8 | description: "Whether the PR is from a foreign repository" 9 | value: ${{ steps.state.outputs.foreign }} 10 | is_pr: 11 | description: "Whether the event is a pull request" 12 | value: ${{ steps.state.outputs.is_pr }} 13 | 14 | runs: 15 | using: "composite" 16 | steps: 17 | - name: Determine repository state 18 | id: state 19 | run: | 20 | set -x 21 | protected=${{ github.ref_protected }} 22 | foreign=${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} 23 | is_pr=${{ github.event_name == 'pull_request' }} 24 | 25 | echo "protected=${protected}" | tee -a $GITHUB_OUTPUT 26 | echo "foreign=${foreign}" | tee -a $GITHUB_OUTPUT 27 | echo "is_pr=${is_pr}" | tee -a $GITHUB_OUTPUT 28 | shell: bash 29 | -------------------------------------------------------------------------------- /man/check_arg.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/02_declare.R 3 | \name{check_output} 4 | \alias{check_output} 5 | \alias{check_arg} 6 | \title{Check Argument Types and Return Type} 7 | \usage{ 8 | check_output(.output, .assertion, ...) 9 | 10 | check_arg(.arg, .assertion, ..., .bind = FALSE) 11 | } 12 | \arguments{ 13 | \item{.output}{function output} 14 | 15 | \item{.assertion}{an assertion} 16 | 17 | \item{...}{additional arguments passed to assertion} 18 | 19 | \item{.arg}{function argument} 20 | 21 | \item{.bind}{whether to actively bind the argument so it cannot be modified 22 | unless it satisfies the assertion} 23 | } 24 | \value{ 25 | \code{.output}if it satisfies the assertion, fails otherwise. 26 | 27 | returns \code{NULL} invisibly, called for side effects. 28 | } 29 | \description{ 30 | These functions are not designed to be used directly, we advise to use the 31 | syntaxes described in \code{?declare} instead. \code{check_arg} checks that arguments 32 | satisfy an assertion, and if relevant make them into active bindings to make sure they 33 | always satisy it. \code{check_output} checks that the value, presumably a return 34 | value, satisfies an assertion, 35 | } 36 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Test environments 2 | * local R installation, R 4.0.2 3 | * ubuntu 16.04 (on travis-ci), R 4.0.2 4 | * win-builder (devel) 5 | 6 | ## R CMD check results 7 | 8 | 0 errors | 0 warnings | 1 note 9 | 10 | * This is a new release. 11 | 12 | ## Answering comments from Uwe Ligges 2021-03-15 13 | 14 | The broken URL in the README was fixed. 15 | The package description was rewritten to include more details. 16 | 17 | ## Answering comments from Uwe Ligges 2021-03-16 18 | 19 | The description was updated and doesn't start with the package's name. 20 | 21 | > Also: Is there some reference about the method you can add in the 22 | > Description field in the form Authors (year) ? 23 | 24 | No there's no doi, the package is not based on any published article, it takes 25 | inspiration from previous work credited at the end of the README but the bulk of 26 | the work is original and all the work lives in the package only at the moment. 27 | 28 | ## Answering comments from Gregor Seyer 2021-03-18 29 | 30 | The title has been updated to be less than 65 characters 31 | 32 | \Value was added for every exported function (doc was slightly reorganised too), 33 | with relevant explanations regarding class and output meaning. 34 | -------------------------------------------------------------------------------- /.github/workflows/style/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to auto-style a package" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Check air.toml 7 | id: check 8 | run: | 9 | set -x 10 | if [ -f air.toml ]; then 11 | echo air_enabled=true >> $GITHUB_OUTPUT 12 | else 13 | echo air_enabled=false >> $GITHUB_OUTPUT 14 | fi 15 | if [ -f .clang-format ]; then 16 | echo clang_format_enabled=true >> $GITHUB_OUTPUT 17 | else 18 | echo clang_format_enabled=false >> $GITHUB_OUTPUT 19 | fi 20 | shell: bash 21 | 22 | - name: Install air 23 | if: ${{ steps.check.outputs.air_enabled == 'true' }} 24 | uses: posit-dev/setup-air@v1 25 | 26 | - name: Run air 27 | if: ${{ steps.check.outputs.air_enabled == 'true' }} 28 | run: | 29 | air format . 30 | shell: bash 31 | 32 | - name: Run clang-format 33 | if: ${{ steps.check.outputs.clang_format_enabled == 'true' }} 34 | run: | 35 | shopt -s nullglob 36 | clang-format -i src/*.{c,cc,cpp,h,hpp} 37 | shell: bash 38 | 39 | - name: Check Git status 40 | if: ${{ steps.check.outputs.air_enabled == 'true' || steps.check.outputs.clang_format_enabled == 'true' }} 41 | run: | 42 | git status 43 | shell: bash 44 | -------------------------------------------------------------------------------- /.github/workflows/covr/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to run covr for an R package" 2 | description: "Run covr to check code coverage for an R package and upload results to Codecov." 3 | inputs: 4 | token: 5 | description: codecov token 6 | required: false 7 | 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Run coverage check 12 | run: | 13 | if (dir.exists("tests/testthat")) { 14 | cov <- covr::package_coverage( 15 | quiet = FALSE, 16 | clean = FALSE, 17 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") 18 | ) 19 | covr::to_cobertura(cov) 20 | } else { 21 | message("No tests found, coverage not tested.") 22 | } 23 | shell: Rscript {0} 24 | 25 | - uses: codecov/codecov-action@v5 26 | with: 27 | # Fail if token is given 28 | fail_ci_if_error: ${{ inputs.token != '' }} 29 | files: ./cobertura.xml 30 | plugins: noop 31 | disable_search: true 32 | token: ${{ inputs.token }} 33 | 34 | - name: Show testthat output 35 | if: always() 36 | run: | 37 | ## -------------------------------------------------------------------- 38 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 39 | shell: bash 40 | 41 | - name: Upload test results 42 | if: failure() 43 | uses: actions/upload-artifact@v5 44 | with: 45 | name: coverage-test-failures 46 | path: ${{ runner.temp }}/package 47 | -------------------------------------------------------------------------------- /tests/testthat/test-declare.R: -------------------------------------------------------------------------------- 1 | test_that("check_output works", { 2 | expect_equal(check_output(2, Double()), 2) 3 | expect_error(check_output(2L, Double())) 4 | }) 5 | 6 | test_that("check_arg works", { 7 | x <- 1 8 | y <- 2 9 | expect_equal(check_arg(x, Double()), NULL) 10 | expect_equal(check_arg(y, Double(), .bind = TRUE), NULL) 11 | expect_equal(y <- 3, 3) 12 | expect_error(y <- 3L) 13 | # commenting because according to rhub doesn't work on 14 | # Windows Server 2008 R2 SP1, R-devel, 32/64 bit 15 | # expect_error(check_arg(x, Integer())) 16 | # expect_error(check_arg(y, Integer(), .bind = TRUE)) 17 | }) 18 | 19 | test_that("declare works", { 20 | expect_equal(declare("x", value = 1), 1) 21 | expect_equal(declare("x", value = data.frame(a=1)), data.frame(a=1)) 22 | sys_time <- Sys.time() 23 | expect_equal(declare("x", value = sys_time), sys_time) 24 | foobar_obj <- structure(1, class = c("foo", "bar")) 25 | expect_equal(declare("x", value = foobar_obj), foobar_obj) 26 | #expect_error(? x <- stop("!!!")) 27 | 28 | expect_equal(declare("x", Double(), value = 1), 1) 29 | expect_equal(declare("x", Double, value = 1), 1) 30 | expect_equal(x <- 2, 2) 31 | expect_error(x <- 2L) 32 | expect_error(declare("x", Double(), value = 1L)) 33 | 34 | expect_equal(declare("x", Double(), value = 1, const = TRUE), 1) 35 | expect_equal(x, 1) 36 | expect_error(x <- 2) 37 | expect_error(declare("x", Double(), value = 1L, const = TRUE)) 38 | 39 | expect_equal(Double() ? x <- 1, 1) 40 | expect_error(Double() ? x <- 1L) 41 | }) 42 | 43 | test_that("values are declared in separate environments", { 44 | typed::Integer() ? a 45 | typed::Integer() ? b 46 | 47 | a <- 1L 48 | b <- 2L 49 | expect_equal(a, 1L) 50 | }) 51 | -------------------------------------------------------------------------------- /tests/testthat/test-qm.R: -------------------------------------------------------------------------------- 1 | test_that("question mark works", { 2 | # outside of function def 3 | expect_equal(? x <- 1, 1) 4 | expect_equal(? (x) <- 1, 1) 5 | expect_equal(Double() ? x, NULL) 6 | expect_equal(Double() ? x <- 1, 1) 7 | expect_equal(Double() ? (x) <- 1, 1) 8 | expect_equal((Double() ? x = 1), 1) 9 | 10 | # regular help 11 | expect_error(?mean, NA) 12 | 13 | # function def 14 | expect_error( 15 | regexp=NA, 16 | fun <- Double() ? function(x = ?~ Symbol(), y = ?+ Double(), z = 1 ? Double()) { 17 | ?mean 18 | ? foo <- 1 19 | ? (foo) <- 1 20 | Double() ? bar <- 1 21 | Double() ? (bar) <- 1 22 | Double() ? baz 23 | baz <- 1 24 | if(TRUE) return(foo) 25 | foo 26 | }) 27 | expect_error( 28 | regexp=NA, 29 | ? function(...= ? Double()) {} 30 | ) 31 | expect_error( 32 | ? function(...= ?+ Double()) {} 33 | ) 34 | expect_error( 35 | regexp=NA, 36 | ? function(...= ?~ Double()) {} 37 | ) 38 | expect_error( 39 | regexp=NA, 40 | ? function(...= ? Dots(2)) {} 41 | ) 42 | expect_error( 43 | regexp=NA, 44 | ? function(...= ?~ Dots(2)) {} 45 | ) 46 | expect_error( 47 | regexp=NA, 48 | Function() ? fun1 <- Double() ? function() {1} 49 | ) 50 | }) 51 | 52 | test_that("Irrelevant return() calls are not wrapped", { 53 | expect_error({ 54 | fun <- typed::Function() ? function() { 55 | function() { 56 | return() 57 | } 58 | } 59 | }, NA) 60 | 61 | expect_error({ 62 | fun <- typed::Integer() ? function() { 63 | ret <- local({ 64 | return(1.0) 65 | }) 66 | as.integer(10) 67 | } 68 | }, NA) 69 | }) 70 | # detach("package:typed");covr::report() 71 | 72 | # `?` <- typed::`?` 73 | 74 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Also included in R-CMD-check.yaml, this workflow only listens to pushes to branches 3 | # that start with "docs*" or "cran-*" and does not need to act on pushes to the main branch. 4 | on: 5 | push: 6 | branches: 7 | - "docs*" 8 | - "cran-*" 9 | # The main branch is excluded here, it is handled by the R-CMD-check workflow. 10 | # This workflow is only for handling pushes to designated branches. 11 | workflow_dispatch: 12 | 13 | name: pkgdown 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.sha }}-${{ github.base_ref || '' }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | pkgdown: 21 | runs-on: ubuntu-24.04 22 | 23 | name: "pkgdown" 24 | 25 | # Begin custom: services 26 | # End custom: services 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - uses: ./.github/workflows/rate-limit 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - uses: ./.github/workflows/git-identity 36 | if: github.event_name == 'push' 37 | 38 | - uses: ./.github/workflows/custom/before-install 39 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 40 | 41 | - uses: ./.github/workflows/install 42 | with: 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | cache-version: pkgdown-2 45 | needs: website 46 | extra-packages: r-lib/pkgdown local::. 47 | 48 | - uses: ./.github/workflows/custom/after-install 49 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 50 | 51 | - uses: ./.github/workflows/pkgdown-build 52 | if: github.event_name != 'push' 53 | 54 | - uses: ./.github/workflows/pkgdown-deploy 55 | if: github.event_name == 'push' 56 | -------------------------------------------------------------------------------- /.github/workflows/format-suggest.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/posit-dev/setup-air/tree/main/examples 2 | 3 | on: 4 | # Using `pull_request_target` over `pull_request` for elevated `GITHUB_TOKEN` 5 | # privileges, otherwise we can't set `pull-requests: write` when the pull 6 | # request comes from a fork, which is our main use case (external contributors). 7 | # 8 | # `pull_request_target` runs in the context of the target branch (`main`, usually), 9 | # rather than in the context of the pull request like `pull_request` does. Due 10 | # to this, we must explicitly checkout `ref: ${{ github.event.pull_request.head.sha }}`. 11 | # This is typically frowned upon by GitHub, as it exposes you to potentially running 12 | # untrusted code in a context where you have elevated privileges, but they explicitly 13 | # call out the use case of reformatting and committing back / commenting on the PR 14 | # as a situation that should be safe (because we aren't actually running the untrusted 15 | # code, we are just treating it as passive data). 16 | # https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ 17 | pull_request_target: 18 | 19 | name: format-suggest.yaml 20 | 21 | jobs: 22 | format-suggest: 23 | name: format-suggest 24 | runs-on: ubuntu-latest 25 | # Only run this job if changes come from a fork. 26 | # We commit changes directly on the main repository. 27 | if: github.event.pull_request.head.repo.full_name != github.repository 28 | 29 | permissions: 30 | # Required to push suggestion comments to the PR 31 | pull-requests: write 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | with: 36 | ref: ${{ github.event.pull_request.head.sha }} 37 | 38 | - uses: ./.github/workflows/style 39 | 40 | - name: Suggest 41 | uses: reviewdog/action-suggester@v1 42 | with: 43 | level: error 44 | fail_level: error 45 | tool_name: air-and-clang-format 46 | -------------------------------------------------------------------------------- /docs/bootstrap-toc.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Table of Contents v0.4.1 (http://afeld.github.io/bootstrap-toc/) 3 | * Copyright 2015 Aidan Feldman 4 | * Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */ 5 | 6 | /* modified from https://github.com/twbs/bootstrap/blob/94b4076dd2efba9af71f0b18d4ee4b163aa9e0dd/docs/assets/css/src/docs.css#L548-L601 */ 7 | 8 | /* All levels of nav */ 9 | nav[data-toggle='toc'] .nav > li > a { 10 | display: block; 11 | padding: 4px 20px; 12 | font-size: 13px; 13 | font-weight: 500; 14 | color: #767676; 15 | } 16 | nav[data-toggle='toc'] .nav > li > a:hover, 17 | nav[data-toggle='toc'] .nav > li > a:focus { 18 | padding-left: 19px; 19 | color: #563d7c; 20 | text-decoration: none; 21 | background-color: transparent; 22 | border-left: 1px solid #563d7c; 23 | } 24 | nav[data-toggle='toc'] .nav > .active > a, 25 | nav[data-toggle='toc'] .nav > .active:hover > a, 26 | nav[data-toggle='toc'] .nav > .active:focus > a { 27 | padding-left: 18px; 28 | font-weight: bold; 29 | color: #563d7c; 30 | background-color: transparent; 31 | border-left: 2px solid #563d7c; 32 | } 33 | 34 | /* Nav: second level (shown on .active) */ 35 | nav[data-toggle='toc'] .nav .nav { 36 | display: none; /* Hide by default, but at >768px, show it */ 37 | padding-bottom: 10px; 38 | } 39 | nav[data-toggle='toc'] .nav .nav > li > a { 40 | padding-top: 1px; 41 | padding-bottom: 1px; 42 | padding-left: 30px; 43 | font-size: 12px; 44 | font-weight: normal; 45 | } 46 | nav[data-toggle='toc'] .nav .nav > li > a:hover, 47 | nav[data-toggle='toc'] .nav .nav > li > a:focus { 48 | padding-left: 29px; 49 | } 50 | nav[data-toggle='toc'] .nav .nav > .active > a, 51 | nav[data-toggle='toc'] .nav .nav > .active:hover > a, 52 | nav[data-toggle='toc'] .nav .nav > .active:focus > a { 53 | padding-left: 28px; 54 | font-weight: 500; 55 | } 56 | 57 | /* from https://github.com/twbs/bootstrap/blob/e38f066d8c203c3e032da0ff23cd2d6098ee2dd6/docs/assets/css/src/docs.css#L631-L634 */ 58 | nav[data-toggle='toc'] .nav > .active > ul { 59 | display: block; 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/dep-suggests-matrix/action.R: -------------------------------------------------------------------------------- 1 | # FIXME: Dynamic lookup by parsing https://svn.r-project.org/R/tags/ 2 | get_deps <- function() { 3 | # Determine package dependencies 4 | if (!requireNamespace("desc", quietly = TRUE)) { 5 | install.packages("desc") 6 | } 7 | 8 | deps_df <- desc::desc_get_deps() 9 | deps_df_optional <- deps_df$package[deps_df$type %in% c("Suggests", "Enhances")] 10 | deps_df_hard <- deps_df$package[deps_df$type %in% c("Depends", "Imports", "LinkingTo")] 11 | deps_df_base <- unlist(tools::standard_package_names(), use.names = FALSE) 12 | 13 | packages <- sort(deps_df_optional) 14 | packages <- intersect(packages, rownames(available.packages())) 15 | 16 | # Too big to fail, or can't be avoided: 17 | off_limits <- c("testthat", "rmarkdown", "rcmdcheck", deps_df_hard, deps_df_base) 18 | off_limits_dep <- unlist(tools::package_dependencies(off_limits, recursive = TRUE, which = "strong")) 19 | setdiff(packages, c(off_limits, off_limits_dep)) 20 | } 21 | 22 | if (Sys.getenv("GITHUB_BASE_REF") != "") { 23 | print(Sys.getenv("GITHUB_BASE_REF")) 24 | system("git fetch origin ${GITHUB_BASE_REF}") 25 | # Use .. to avoid having to fetch the entire history 26 | # https://github.com/krlmlr/actions-sync/issues/45 27 | diff_cmd <- "git diff origin/${GITHUB_BASE_REF}.. -- R/ tests/ | egrep '^[+][^+]' | grep -q ::" 28 | diff_lines <- system(diff_cmd, intern = TRUE) 29 | if (length(diff_lines) > 0) { 30 | writeLines("Changes using :: in R/ or tests/:") 31 | writeLines(diff_lines) 32 | packages <- get_deps() 33 | } else { 34 | writeLines("No changes using :: found in R/ or tests/, not checking without suggested packages") 35 | packages <- character() 36 | } 37 | } else { 38 | writeLines("No GITHUB_BASE_REF, checking without suggested packages") 39 | packages <- get_deps() 40 | } 41 | 42 | if (length(packages) > 0) { 43 | json <- paste0( 44 | '{"package":[', 45 | paste0('"', packages, '"', collapse = ","), 46 | "]}" 47 | ) 48 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 49 | writeLines(json) 50 | } else { 51 | writeLines("No suggested packages found.") 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/copilot-setup-steps.yaml: -------------------------------------------------------------------------------- 1 | name: "Copilot Setup Steps" 2 | 3 | # Automatically run the setup steps when they are changed to allow for easy validation, and 4 | # allow manual testing through the repository's "Actions" tab 5 | on: 6 | workflow_dispatch: 7 | push: 8 | paths: 9 | - .github/workflows/copilot-setup-steps.yml 10 | pull_request: 11 | paths: 12 | - .github/workflows/copilot-setup-steps.yml 13 | 14 | jobs: 15 | # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. 16 | copilot-setup-steps: 17 | runs-on: ubuntu-latest 18 | 19 | # Set the permissions to the lowest permissions possible needed for your steps. 20 | # Copilot will be given its own token for its operations. 21 | permissions: 22 | # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. 23 | contents: read 24 | 25 | # You can define any steps you want, and they will run before the agent starts. 26 | # If you do not check out your code, Copilot will do this for you. 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v5 30 | - uses: ./.github/workflows/custom/before-install 31 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 32 | 33 | - uses: ./.github/workflows/install 34 | with: 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | cache-version: copilot 37 | needs: build, check, website 38 | # Beware of using dev pkgdown here, has brought in dev dependencies in the past 39 | extra-packages: any::rcmdcheck r-lib/roxygen2 any::decor r-lib/styler r-lib/pkgdown deps::. 40 | 41 | - uses: ./.github/workflows/custom/after-install 42 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 43 | 44 | # Must come after the custom after-install workflow 45 | - name: Install package 46 | run: | 47 | UserNM=true R CMD INSTALL . || true 48 | shell: bash 49 | 50 | - name: Install air 51 | uses: posit-dev/setup-air@v1 52 | -------------------------------------------------------------------------------- /docs/docsearch.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | // register a handler to move the focus to the search bar 4 | // upon pressing shift + "/" (i.e. "?") 5 | $(document).on('keydown', function(e) { 6 | if (e.shiftKey && e.keyCode == 191) { 7 | e.preventDefault(); 8 | $("#search-input").focus(); 9 | } 10 | }); 11 | 12 | $(document).ready(function() { 13 | // do keyword highlighting 14 | /* modified from https://jsfiddle.net/julmot/bL6bb5oo/ */ 15 | var mark = function() { 16 | 17 | var referrer = document.URL ; 18 | var paramKey = "q" ; 19 | 20 | if (referrer.indexOf("?") !== -1) { 21 | var qs = referrer.substr(referrer.indexOf('?') + 1); 22 | var qs_noanchor = qs.split('#')[0]; 23 | var qsa = qs_noanchor.split('&'); 24 | var keyword = ""; 25 | 26 | for (var i = 0; i < qsa.length; i++) { 27 | var currentParam = qsa[i].split('='); 28 | 29 | if (currentParam.length !== 2) { 30 | continue; 31 | } 32 | 33 | if (currentParam[0] == paramKey) { 34 | keyword = decodeURIComponent(currentParam[1].replace(/\+/g, "%20")); 35 | } 36 | } 37 | 38 | if (keyword !== "") { 39 | $(".contents").unmark({ 40 | done: function() { 41 | $(".contents").mark(keyword); 42 | } 43 | }); 44 | } 45 | } 46 | }; 47 | 48 | mark(); 49 | }); 50 | }); 51 | 52 | /* Search term highlighting ------------------------------*/ 53 | 54 | function matchedWords(hit) { 55 | var words = []; 56 | 57 | var hierarchy = hit._highlightResult.hierarchy; 58 | // loop to fetch from lvl0, lvl1, etc. 59 | for (var idx in hierarchy) { 60 | words = words.concat(hierarchy[idx].matchedWords); 61 | } 62 | 63 | var content = hit._highlightResult.content; 64 | if (content) { 65 | words = words.concat(content.matchedWords); 66 | } 67 | 68 | // return unique words 69 | var words_uniq = [...new Set(words)]; 70 | return words_uniq; 71 | } 72 | 73 | function updateHitURL(hit) { 74 | 75 | var words = matchedWords(hit); 76 | var url = ""; 77 | 78 | if (hit.anchor) { 79 | url = hit.url_without_anchor + '?q=' + escape(words.join(" ")) + '#' + hit.anchor; 80 | } else { 81 | url = hit.url + '?q=' + escape(words.join(" ")); 82 | } 83 | 84 | return url; 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/versions-matrix/action.R: -------------------------------------------------------------------------------- 1 | # Determine active versions of R to test against 2 | tags <- xml2::read_html("https://svn.r-project.org/R/tags/") 3 | 4 | bullets <- 5 | tags |> 6 | xml2::xml_find_all("//li") |> 7 | xml2::xml_text() 8 | 9 | version_bullets <- grep("^R-([0-9]+-[0-9]+-[0-9]+)/$", bullets, value = TRUE) 10 | versions <- unique(gsub("^R-([0-9]+)-([0-9]+)-[0-9]+/$", "\\1.\\2", version_bullets)) 11 | 12 | r_release <- head(sort(as.package_version(versions), decreasing = TRUE), 5) 13 | 14 | deps <- desc::desc_get_deps() 15 | r_crit <- deps$version[deps$package == "R"] 16 | if (length(r_crit) == 1) { 17 | min_r <- as.package_version(gsub("^>= ([0-9]+[.][0-9]+)(?:.*)$", "\\1", r_crit)) 18 | r_release <- r_release[r_release >= min_r] 19 | } 20 | 21 | r_versions <- c("devel", as.character(r_release)) 22 | 23 | macos <- data.frame(os = "macos-latest", r = r_versions[2:3]) 24 | windows <- data.frame(os = "windows-latest", r = r_versions[1:3]) 25 | linux_devel <- data.frame(os = "ubuntu-22.04", r = r_versions[1], `http-user-agent` = "release", check.names = FALSE) 26 | linux <- data.frame(os = "ubuntu-22.04", r = r_versions[-1]) 27 | covr <- data.frame(os = "ubuntu-22.04", r = r_versions[2], covr = "true", desc = "with covr") 28 | 29 | include_list <- list(macos, windows, linux_devel, linux, covr) 30 | 31 | if (file.exists(".github/versions-matrix.R")) { 32 | custom <- source(".github/versions-matrix.R")$value 33 | if (is.data.frame(custom)) { 34 | custom <- list(custom) 35 | } 36 | include_list <- c(include_list, custom) 37 | } 38 | 39 | print(include_list) 40 | 41 | filter <- read.dcf("DESCRIPTION")[1, ]["Config/gha/filter"] 42 | if (!is.na(filter)) { 43 | filter_expr <- parse(text = filter)[[1]] 44 | subset_fun_expr <- bquote(function(x) subset(x, .(filter_expr))) 45 | subset_fun <- eval(subset_fun_expr) 46 | include_list <- lapply(include_list, subset_fun) 47 | print(include_list) 48 | } 49 | 50 | to_json <- function(x) { 51 | if (nrow(x) == 0) return(character()) 52 | parallel <- vector("list", length(x)) 53 | for (i in seq_along(x)) { 54 | parallel[[i]] <- paste0('"', names(x)[[i]], '":"', x[[i]], '"') 55 | } 56 | paste0("{", do.call(paste, c(parallel, sep = ",")), "}") 57 | } 58 | 59 | configs <- unlist(lapply(include_list, to_json)) 60 | json <- paste0('{"include":[', paste(configs, collapse = ","), "]}") 61 | 62 | if (Sys.getenv("GITHUB_OUTPUT") != "") { 63 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 64 | } 65 | writeLines(json) 66 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check-status.yaml: -------------------------------------------------------------------------------- 1 | # Workflow to update the status of a commit for the R-CMD-check workflow 2 | # Necessary because remote PRs cannot update the status of the commit 3 | on: 4 | workflow_run: 5 | workflows: 6 | - rcc 7 | types: 8 | - requested 9 | - completed 10 | 11 | name: rcc-status 12 | 13 | jobs: 14 | rcc-status: 15 | runs-on: ubuntu-24.04 16 | 17 | name: "Update commit status" 18 | 19 | permissions: 20 | contents: read 21 | statuses: write 22 | 23 | steps: 24 | - name: "Update commit status" 25 | # Only run if triggered by rcc workflow 26 | if: github.event.workflow_run.name == 'rcc' 27 | env: 28 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | run: | 30 | set -x 31 | 32 | if [ "${{ github.event.workflow_run.status }}" == "completed" ]; then 33 | if [ "${{ github.event.workflow_run.conclusion }}" == "success" ]; then 34 | state="success" 35 | else 36 | state="failure" 37 | fi 38 | 39 | # Read artifact ID 40 | artifact_id=$(gh api \ 41 | -H "Accept: application/vnd.github+json" \ 42 | -H "X-GitHub-Api-Version: 2022-11-28" \ 43 | repos/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}/artifacts | jq -r '.artifacts[] | select(.name == "rcc-smoke-sha") | .id') 44 | 45 | if [ -n "${artifact_id}" ]; then 46 | # Download artifact 47 | curl -L -o rcc-smoke-sha.zip \ 48 | -H "Accept: application/vnd.github+json" \ 49 | -H "Authorization: Bearer ${GH_TOKEN}" \ 50 | -H "X-GitHub-Api-Version: 2022-11-28" \ 51 | https://api.github.com/repos/${{ github.repository }}/actions/artifacts/${artifact_id}/zip 52 | 53 | # Unzip artifact 54 | unzip rcc-smoke-sha.zip 55 | 56 | # Read artifact 57 | sha=$(cat rcc-smoke-sha.txt) 58 | 59 | # Clean up 60 | rm rcc-smoke-sha.zip rcc-smoke-sha.txt 61 | fi 62 | else 63 | state="pending" 64 | fi 65 | 66 | if [ -z "${sha}" ]; then 67 | sha=${{ github.event.workflow_run.head_sha }} 68 | fi 69 | 70 | html_url=${{ github.event.workflow_run.html_url }} 71 | description=${{ github.event.workflow_run.name }} 72 | 73 | gh api \ 74 | --method POST \ 75 | -H "Accept: application/vnd.github+json" \ 76 | -H "X-GitHub-Api-Version: 2022-11-28" \ 77 | repos/${{ github.repository }}/statuses/${sha} \ 78 | -f "state=${state}" -f "target_url=${html_url}" -f "description=${description}" -f "context=rcc" 79 | shell: bash 80 | -------------------------------------------------------------------------------- /.github/workflows/commit-suggest.yaml: -------------------------------------------------------------------------------- 1 | name: commit-suggest.yaml 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["rcc"] 6 | types: 7 | - completed 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | jobs: 14 | commit-suggest: 15 | runs-on: ubuntu-latest 16 | if: github.event.workflow_run.event == 'pull_request' 17 | 18 | steps: 19 | - name: Show event payload 20 | run: | 21 | echo '${{ toJson(github.event) }}' | jq . 22 | shell: bash 23 | 24 | - name: Checkout PR 25 | uses: actions/checkout@v4 26 | with: 27 | ref: ${{ github.event.workflow_run.head_sha }} 28 | 29 | - name: Download artifact 30 | uses: actions/download-artifact@v6 31 | with: 32 | name: changes-patch 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | run-id: ${{ github.event.workflow_run.id }} 35 | continue-on-error: true 36 | 37 | - name: Check if artifact exists 38 | id: check-artifact 39 | run: | 40 | if [ -f changes.patch ]; then 41 | echo "has_diff=true" >> $GITHUB_OUTPUT 42 | else 43 | echo "has_diff=false" >> $GITHUB_OUTPUT 44 | echo "No changes-patch artifact found" 45 | fi 46 | shell: bash 47 | 48 | - name: Find PR number for branch from correct head repository 49 | id: find-pr 50 | env: 51 | GITHUB_TOKEN: ${{ github.token }} 52 | run: | 53 | PR_NUMBER=$(gh pr list --head ${{ github.event.workflow_run.head_branch }} --state open --json number,headRepositoryOwner --jq '.[] | select(.headRepositoryOwner.login == "${{ github.event.workflow_run.head_repository.owner.login }}") | .number' || echo "") 54 | echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT 55 | shell: bash 56 | 57 | - name: Generate comment body 58 | if: steps.check-artifact.outputs.has_diff == 'true' 59 | id: comment-body 60 | run: | 61 | cat << 'EOF' > comment.md 62 | ## Formatting suggestions available 63 | 64 | A patch file with formatting suggestions has been generated. You can apply it using one of these methods: 65 | 66 | ### Method 1: Apply via gh CLI 67 | 68 | ```bash 69 | # Download and apply the patch directly 70 | gh run download ${{ github.event.workflow_run.id }} --repo ${{ github.repository }} --name changes-patch && patch -p1 < changes.patch && rm changes.patch 71 | ``` 72 | 73 | ### Method 2: View the patch 74 | 75 |
76 | Click to see the patch contents 77 | 78 | ```diff 79 | EOF 80 | 81 | cat changes.patch >> comment.md 82 | 83 | cat << 'EOF' >> comment.md 84 | ``` 85 | 86 |
87 | 88 | --- 89 | *This comment was automatically generated by the commit-suggester workflow.* 90 | EOF 91 | shell: bash 92 | 93 | - name: Post or update comment 94 | if: steps.check-artifact.outputs.has_diff == 'true' 95 | uses: thollander/actions-comment-pull-request@v3 96 | with: 97 | pr-number: ${{ steps.find-pr.outputs.pr_number }} 98 | file-path: comment.md 99 | comment-tag: formatting-suggestions 100 | mode: recreate 101 | -------------------------------------------------------------------------------- /man/declare.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/01_question_mark.R, R/02_declare.R 3 | \name{?} 4 | \alias{?} 5 | \alias{declare} 6 | \title{Set Variable Types, Argument Types and Return Types.} 7 | \usage{ 8 | `?`(lhs, rhs) 9 | 10 | declare(x, assertion, value, const = FALSE) 11 | } 12 | \arguments{ 13 | \item{lhs}{lhs} 14 | 15 | \item{rhs}{rhs} 16 | 17 | \item{x}{variable name as a string} 18 | 19 | \item{assertion}{a function} 20 | 21 | \item{value}{an optional value} 22 | 23 | \item{const}{whether to declare \code{x} as a constant} 24 | } 25 | \value{ 26 | \code{declare} (and \verb{?} when it maps to \code{declare}) returns \code{value} invisibly, it is called for side effects. 27 | \verb{assertion ? function() \{\}} returns a typed function, of class \code{c("typed", "function")}. 28 | \verb{fun <- assertion ? function() \{\}} returns a typed function and 29 | binds it to \code{fun} in the local environment. 30 | } 31 | \description{ 32 | Use \verb{?} to set a function's return type, argument types, or variable types 33 | in the body of the function. \code{declare} is an alternative to set a variable's 34 | type. 35 | } 36 | \section{Set A Variable's Type}{ 37 | 38 | 39 | When used to set a variable's type, \verb{?} maps 40 | to \code{declare} so that \code{assertion ? var} calls \code{declare("var", assertion)}, 41 | \code{assertion ? var <- value} calls \code{declare("var", assertion, value)}, and 42 | \code{assertion ? (var) <- value} calls \code{declare("var", assertion, value, const = TRUE)} 43 | 44 | In those cases an active binding is defined so \code{var} returns \code{value} (or 45 | \code{NULL} if none was provided). If \code{const} is \code{FALSE} (the default), the 46 | returned value can then be altered if by assigning to \code{var}, but a value which 47 | doesn't satisfy the assertion will trigger an error. 48 | } 49 | 50 | \section{Set A Function's Return Type}{ 51 | 52 | 53 | The syntaxes \verb{assertion ? function() \{\}} and \verb{fun <- assertion ? function() \{\}} 54 | can be used to create a function of class \code{c("typed", "function")}. 55 | The returned function will have its body modified so that return values are 56 | wrapped inside a \code{check_output()} call. Printing the function will display 57 | the return type. 58 | } 59 | 60 | \section{Set A Function Argument's Type}{ 61 | 62 | 63 | When using the above syntax, or if we don't want to force a return type, the 64 | simpler \verb{? function() \{\}} or \verb{fun <- ? function() \{\}} 65 | syntax, we can set argument types by providing arguments as \code{arg = default_value ? assertion} or 66 | \code{arg = ? assertion}. When entering the function, argument types will be checked. 67 | 68 | By default the arguments are only checked at the top, and might be assigned later 69 | in the function's body values that don't satisfy the assertion, to avoid this 70 | we can type \code{arg = default_value ? +assertion} or \code{arg = ? +assertion}. 71 | 72 | Note that forgetting the \verb{?} before \code{function} is an easy mistake to do! 73 | 74 | If we'd rather check the quoted argument rather than the argument's value, 75 | we can type \code{arg = default_value ? ~assertion} or 76 | \code{arg = ? ~assertion}. A possible use case might be \code{arg = ? ~ Symbol()}. 77 | 78 | Dots can be checked too, \code{... = ? assertion} will make sure that every argument 79 | passed to dots satisfies the assertion. 80 | 81 | The special assertion factory \code{Dots} can also be used, in that case the checks will 82 | apply to \code{list(...)} rather than to each element individually, for instance 83 | \verb{function(... = ? Dots(2))} makes sure the dots were fed 2 values. 84 | 85 | The returned function will have its body modified so the arguments are 86 | checked by \code{check_arg()} calls at the top. Printing the function will display 87 | the argument types. 88 | } 89 | 90 | \examples{ 91 | Integer() ? function (x= ? Integer()) { 92 | Integer() ? y <- 2L 93 | res <- x + y 94 | res 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /.github/workflows/pr-commands.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | issue_comment: 3 | types: [created] 4 | name: Commands 5 | jobs: 6 | document: 7 | if: startsWith(github.event.comment.body, '/document') 8 | name: document 9 | # macos is actually better here due to native binary packages 10 | runs-on: macos-latest 11 | env: 12 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: r-lib/actions/pr-fetch@v2 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | - uses: r-lib/actions/setup-r@v2 19 | - name: Configure Git identity 20 | run: | 21 | env | sort 22 | git config --local user.name "$GITHUB_ACTOR" 23 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 24 | shell: bash 25 | - name: Install dependencies 26 | run: | 27 | install.packages(c("remotes", "roxygen2"), type = "binary") 28 | remotes::install_deps(dependencies = TRUE) 29 | shell: Rscript {0} 30 | - name: Document 31 | run: | 32 | roxygen2::roxygenise() 33 | shell: Rscript {0} 34 | - name: commit 35 | run: | 36 | if [ -n "$(git status --porcelain man/ NAMESPACE)" ]; then 37 | git add man/ NAMESPACE 38 | git commit -m 'Document' 39 | fi 40 | - uses: r-lib/actions/pr-push@v2 41 | with: 42 | repo-token: ${{ secrets.GITHUB_TOKEN }} 43 | style: 44 | if: startsWith(github.event.comment.body, '/style') 45 | name: style 46 | # macos is actually better here due to native binary packages 47 | runs-on: macos-latest 48 | env: 49 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: r-lib/actions/pr-fetch@v2 53 | with: 54 | repo-token: ${{ secrets.GITHUB_TOKEN }} 55 | - uses: r-lib/actions/setup-r@v2 56 | - name: Configure Git identity 57 | run: | 58 | env | sort 59 | git config --local user.name "$GITHUB_ACTOR" 60 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 61 | shell: bash 62 | - name: Install dependencies 63 | run: | 64 | install.packages(c("styler", "roxygen2"), type = "binary") 65 | shell: Rscript {0} 66 | - name: Style 67 | run: | 68 | styler::style_pkg(strict = FALSE) 69 | shell: Rscript {0} 70 | - name: commit 71 | run: | 72 | if [ -n "$(git status --porcelain '*.R' '*.Rmd')" ]; then 73 | git add '*.R' '*.Rmd' 74 | git commit -m 'Style' 75 | fi 76 | - uses: r-lib/actions/pr-push@v2 77 | with: 78 | repo-token: ${{ secrets.GITHUB_TOKEN }} 79 | merge: 80 | if: startsWith(github.event.comment.body, '/merge') 81 | name: merge 82 | runs-on: ubuntu-22.04 83 | steps: 84 | - name: Create and merge pull request 85 | run: | 86 | set -exo pipefail 87 | PR_DETAILS=$( curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} ) 88 | echo "$PR_DETAILS" | jq . 89 | PR_BASE=$(echo "$PR_DETAILS" | jq -r .base.ref) 90 | PR_HEAD=$(echo "$PR_DETAILS" | jq -r .head.ref) 91 | PR_URL=$(curl -s -X POST --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" --data '{ "head": "'$PR_BASE'", "base": "'$PR_HEAD'", "title": "Merge back PR target branch", "body": "Target: #${{ github.event.issue.number }}" }' https://api.github.com/repos/${{ github.repository }}/pulls | jq -r .url ) 92 | echo $PR_URL 93 | # Merging here won't run CI/CD 94 | # curl -s -X PUT --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" $PR_URL/merge 95 | # A mock job just to ensure we have a successful build status 96 | finish: 97 | runs-on: ubuntu-22.04 98 | steps: 99 | - run: true 100 | -------------------------------------------------------------------------------- /.github/workflows/update-snapshots/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to create pull requests for updated testthat snapshots" 2 | description: > 3 | This action will run `testthat::test_local()` for tests that seem to use snapshots, 4 | this is determined by reading and grepping the test files. 5 | If the tests are failing, snapshots are updated, and a pull request is opened. 6 | inputs: 7 | base: 8 | description: "The base branch to create the pull request against." 9 | required: false 10 | default: "main" 11 | 12 | runs: 13 | using: "composite" 14 | steps: 15 | - name: Run tests on test files that use snapshots 16 | id: run-tests 17 | run: | 18 | ## -- Run tests on test files that use snapshots -- 19 | rx <- "^test-(.*)[.][rR]$" 20 | files <- dir("tests/testthat", pattern = rx) 21 | has_snapshot <- vapply(files, function(.x) any(grepl("snapshot", readLines(file.path("tests/testthat", .x)), fixed = TRUE)), logical(1)) 22 | if (any(has_snapshot)) { 23 | patterns <- gsub(rx, "^\\1$", files[has_snapshot]) 24 | pattern <- paste0(patterns, collapse = "|") 25 | tryCatch( 26 | { 27 | Sys.setenv(TESTTHAT_PARALLEL = FALSE) 28 | result <- as.data.frame(testthat::test_local(pattern = pattern, reporter = "location", stop_on_failure = FALSE)) 29 | failures <- result[result$failed + result$warning > 0, ] 30 | print(failures) 31 | if (nrow(failures) > 0) { 32 | writeLines("Snapshot tests failed/warned.") 33 | print(failures[names(failures) != "result"]) 34 | print(failures$result) 35 | testthat::snapshot_accept() 36 | writeLines("changed=true", Sys.getenv("GITHUB_OUTPUT")) 37 | } else { 38 | writeLines("Snapshot tests ran successfully.") 39 | } 40 | }, 41 | error = print 42 | ) 43 | } else { 44 | writeLines("No snapshots found.") 45 | } 46 | shell: Rscript {0} 47 | 48 | - name: Add snapshots to Git 49 | if: ${{ steps.run-tests.outputs.changed }} 50 | run: | 51 | ## -- Add snapshots to Git -- 52 | mkdir -p tests/testthat/_snaps 53 | git add -- tests/testthat/_snaps 54 | shell: bash 55 | 56 | - name: Check changed files 57 | if: ${{ steps.run-tests.outputs.changed }} 58 | id: check-changed 59 | run: | 60 | set -x 61 | if [ "${{ github.event_name}}" != "pull_request" ] ; then 62 | echo "changed=$(git status --porcelain -- tests/testthat/_snaps | head -n 1)" | tee -a $GITHUB_OUTPUT 63 | fi 64 | shell: bash 65 | 66 | - name: Derive branch name 67 | if: ${{ steps.check-changed.outputs.changed }} 68 | id: matrix-desc 69 | run: | 70 | set -x 71 | config=$(echo '${{ toJSON(matrix) }}' | jq -c .) 72 | echo "text=$(echo ${config})" | tee -a $GITHUB_OUTPUT 73 | echo "branch=$(echo ${config} | sed -r 's/[^0-9a-zA-Z]+/-/g;s/^-//;s/-$//')" | tee -a $GITHUB_OUTPUT 74 | shell: bash 75 | 76 | - name: Create pull request 77 | # Fall through if PR, will use reviewdog/action-suggester in the commit action 78 | if: ${{ steps.check-changed.outputs.changed }} 79 | id: cpr 80 | uses: peter-evans/create-pull-request@v6 81 | with: 82 | base: ${{ inputs.base }} 83 | branch: snapshot-${{ inputs.base }}-${{ github.job }}-${{ steps.matrix-desc.outputs.branch }} 84 | delete-branch: true 85 | title: "test: Snapshot updates for ${{ github.job }} (${{ steps.matrix-desc.outputs.text }})" 86 | body: "Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action${{ github.event.number && format(' for #{0}', github.event.number) || '' }}." 87 | add-paths: | 88 | tests/testthat/_snaps 89 | 90 | - name: Fail if pull request created 91 | if: ${{ steps.cpr.outputs.pull-request-number }} 92 | run: | 93 | false 94 | shell: bash 95 | 96 | - name: Reset Git changes 97 | run: | 98 | git reset -- tests/testthat/_snaps 99 | shell: bash 100 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check-dev.yaml: -------------------------------------------------------------------------------- 1 | # This workflow calls the GitHub API very frequently. 2 | # Can't be run as part of commits 3 | on: 4 | schedule: 5 | - cron: "0 5 * * *" # 05:00 UTC every day only run on main branch 6 | push: 7 | branches: 8 | - "cran-*" 9 | tags: 10 | - "v*" 11 | workflow_dispatch: 12 | 13 | name: rcc dev 14 | 15 | jobs: 16 | matrix: 17 | runs-on: ubuntu-24.04 18 | outputs: 19 | matrix: ${{ steps.set-matrix.outputs.matrix }} 20 | 21 | name: Collect deps 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - uses: ./.github/workflows/rate-limit 27 | with: 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - uses: r-lib/actions/setup-r@v2 31 | 32 | - id: set-matrix 33 | uses: ./.github/workflows/dep-matrix 34 | 35 | check-matrix: 36 | runs-on: ubuntu-24.04 37 | needs: matrix 38 | 39 | name: Check deps 40 | 41 | steps: 42 | - name: Install json2yaml 43 | run: | 44 | sudo npm install -g json2yaml 45 | 46 | - name: Check matrix definition 47 | run: | 48 | matrix='${{ needs.matrix.outputs.matrix }}' 49 | echo $matrix 50 | echo $matrix | jq . 51 | echo $matrix | json2yaml 52 | 53 | R-CMD-check-base: 54 | runs-on: ubuntu-24.04 55 | 56 | name: base 57 | 58 | # Begin custom: services 59 | # End custom: services 60 | 61 | strategy: 62 | fail-fast: false 63 | 64 | steps: 65 | - uses: actions/checkout@v4 66 | 67 | - uses: ./.github/workflows/custom/before-install 68 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 69 | 70 | - uses: ./.github/workflows/install 71 | with: 72 | cache-version: rcc-dev-base-1 73 | needs: build, check 74 | extra-packages: "any::rcmdcheck any::remotes ." 75 | token: ${{ secrets.GITHUB_TOKEN }} 76 | 77 | - name: Session info 78 | run: | 79 | options(width = 100) 80 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") 81 | pkgs <- installed.packages()[, "Package"] 82 | sessioninfo::session_info(pkgs, include_base = TRUE) 83 | shell: Rscript {0} 84 | 85 | - uses: ./.github/workflows/custom/after-install 86 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 87 | 88 | - uses: ./.github/workflows/update-snapshots 89 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository 90 | 91 | - uses: ./.github/workflows/check 92 | with: 93 | results: ${{ matrix.package }} 94 | 95 | R-CMD-check-dev: 96 | needs: 97 | - matrix 98 | - R-CMD-check-base 99 | 100 | runs-on: ubuntu-24.04 101 | 102 | name: 'rcc-dev: ${{ matrix.package }}' 103 | 104 | # Begin custom: services 105 | # End custom: services 106 | 107 | strategy: 108 | fail-fast: false 109 | matrix: ${{fromJson(needs.matrix.outputs.matrix)}} 110 | 111 | steps: 112 | - uses: actions/checkout@v4 113 | 114 | - uses: ./.github/workflows/custom/before-install 115 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 116 | 117 | - uses: ./.github/workflows/install 118 | with: 119 | cache-version: rcc-dev-${{ matrix.package }}-1 120 | needs: build, check 121 | extra-packages: "any::rcmdcheck r-lib/remotes@f-618-universe ." 122 | token: ${{ secrets.GITHUB_TOKEN }} 123 | 124 | - name: Install dev version of ${{ matrix.package }} 125 | env: 126 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 127 | run: | 128 | remotes::install_runiverse("${{ matrix.package }}", linux_distro = "noble") 129 | shell: Rscript {0} 130 | 131 | - name: Session info 132 | run: | 133 | options(width = 100) 134 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") 135 | pkgs <- installed.packages()[, "Package"] 136 | sessioninfo::session_info(pkgs, include_base = TRUE) 137 | shell: Rscript {0} 138 | 139 | - uses: ./.github/workflows/custom/after-install 140 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 141 | 142 | - uses: ./.github/workflows/update-snapshots 143 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository 144 | 145 | - uses: ./.github/workflows/check 146 | with: 147 | results: ${{ matrix.package }} 148 | -------------------------------------------------------------------------------- /.github/workflows/dep-matrix/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to compute a matrix with all dependent packages" 2 | outputs: 3 | matrix: 4 | description: "Generated matrix" 5 | value: ${{ steps.set-matrix.outputs.matrix }} 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - id: set-matrix 11 | run: | 12 | # Determine package dependencies 13 | # From remotes 14 | read_dcf <- function(path) { 15 | fields <- colnames(read.dcf(path)) 16 | as.list(read.dcf(path, keep.white = fields)[1, ]) 17 | } 18 | 19 | re_match <- function(text, pattern, perl = TRUE, ...) { 20 | 21 | stopifnot(is.character(pattern), length(pattern) == 1, !is.na(pattern)) 22 | text <- as.character(text) 23 | 24 | match <- regexpr(pattern, text, perl = perl, ...) 25 | 26 | start <- as.vector(match) 27 | length <- attr(match, "match.length") 28 | end <- start + length - 1L 29 | 30 | matchstr <- substring(text, start, end) 31 | matchstr[ start == -1 ] <- NA_character_ 32 | 33 | res <- data.frame( 34 | stringsAsFactors = FALSE, 35 | .text = text, 36 | .match = matchstr 37 | ) 38 | 39 | if (!is.null(attr(match, "capture.start"))) { 40 | 41 | gstart <- attr(match, "capture.start") 42 | glength <- attr(match, "capture.length") 43 | gend <- gstart + glength - 1L 44 | 45 | groupstr <- substring(text, gstart, gend) 46 | groupstr[ gstart == -1 ] <- NA_character_ 47 | dim(groupstr) <- dim(gstart) 48 | 49 | res <- cbind(groupstr, res, stringsAsFactors = FALSE) 50 | } 51 | 52 | names(res) <- c(attr(match, "capture.names"), ".text", ".match") 53 | class(res) <- c("tbl_df", "tbl", class(res)) 54 | res 55 | } 56 | 57 | dev_split_ref <- function(x) { 58 | re_match(x, "^(?[^@#]+)(?[@#].*)?$") 59 | } 60 | 61 | has_dev_dep <- function(package) { 62 | cran_url <- "https://cloud.r-project.org" 63 | 64 | refs <- dev_split_ref(package) 65 | url <- file.path(cran_url, "web", "packages", refs[["pkg"]], "DESCRIPTION") 66 | 67 | f <- tempfile() 68 | on.exit(unlink(f)) 69 | 70 | utils::download.file(url, f) 71 | desc <- read_dcf(f) 72 | 73 | url_fields <- c(desc$URL, desc$BugReports) 74 | 75 | if (length(url_fields) == 0) { 76 | return(FALSE) 77 | } 78 | 79 | pkg_urls <- unlist(strsplit(url_fields, "[[:space:]]*,[[:space:]]*")) 80 | 81 | # Remove trailing "/issues" from the BugReports URL 82 | pkg_urls <- sub("/issues$", "", pkg_urls) 83 | 84 | valid_domains <- c("github[.]com", "gitlab[.]com", "bitbucket[.]org") 85 | 86 | parts <- 87 | re_match(pkg_urls, 88 | sprintf("^https?://(?%s)/(?%s)/(?%s)(?:/(?%s))?", 89 | domain = paste0(valid_domains, collapse = "|"), 90 | username = "[^/]+", 91 | repo = "[^/@#]+", 92 | subdir = "[^/@$ ]+" 93 | ) 94 | )[c("domain", "username", "repo", "subdir")] 95 | 96 | # Remove cases which don't match and duplicates 97 | 98 | parts <- unique(stats::na.omit(parts)) 99 | 100 | nrow(parts) == 1 101 | } 102 | 103 | if (!requireNamespace("desc", quietly = TRUE)) { 104 | install.packages("desc") 105 | } 106 | 107 | deps_df <- desc::desc_get_deps() 108 | deps_df <- deps_df[deps_df$type %in% c("Depends", "Imports", "LinkingTo", "Suggests"), ] 109 | 110 | packages <- sort(deps_df$package) 111 | packages <- intersect(packages, rownames(available.packages())) 112 | 113 | valid_dev_dep <- vapply(packages, has_dev_dep, logical(1)) 114 | 115 | # https://github.com/r-lib/remotes/issues/576 116 | valid_dev_dep[packages %in% c("igraph", "duckdb", "logging")] <- FALSE 117 | 118 | deps <- packages[valid_dev_dep] 119 | if (any(!valid_dev_dep)) { 120 | msg <- paste0( 121 | "Could not determine development repository for packages: ", 122 | paste(packages[!valid_dev_dep], collapse = ", ") 123 | ) 124 | writeLines(paste0("::warning::", msg)) 125 | } 126 | 127 | json <- paste0( 128 | '{"package":[', 129 | paste0('"', deps, '"', collapse = ","), 130 | ']}' 131 | ) 132 | writeLines(json) 133 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 134 | shell: Rscript {0} 135 | -------------------------------------------------------------------------------- /docs/pkgdown.js: -------------------------------------------------------------------------------- 1 | /* http://gregfranko.com/blog/jquery-best-practices/ */ 2 | (function($) { 3 | $(function() { 4 | 5 | $('nav.navbar').headroom(); 6 | 7 | Toc.init({ 8 | $nav: $("#toc"), 9 | $scope: $("main h2, main h3, main h4, main h5, main h6") 10 | }); 11 | 12 | if ($('#toc').length) { 13 | $('body').scrollspy({ 14 | target: '#toc', 15 | offset: $("nav.navbar").outerHeight() + 1 16 | }); 17 | } 18 | 19 | // Activate popovers 20 | $('[data-bs-toggle="popover"]').popover({ 21 | container: 'body', 22 | html: true, 23 | trigger: 'focus', 24 | placement: "top", 25 | sanitize: false, 26 | }); 27 | 28 | $('[data-bs-toggle="tooltip"]').tooltip(); 29 | 30 | /* Clipboard --------------------------*/ 31 | 32 | function changeTooltipMessage(element, msg) { 33 | var tooltipOriginalTitle=element.getAttribute('data-bs-original-title'); 34 | element.setAttribute('data-bs-original-title', msg); 35 | $(element).tooltip('show'); 36 | element.setAttribute('data-bs-original-title', tooltipOriginalTitle); 37 | } 38 | 39 | if(ClipboardJS.isSupported()) { 40 | $(document).ready(function() { 41 | var copyButton = ""; 42 | 43 | $("div.sourceCode").addClass("hasCopyButton"); 44 | 45 | // Insert copy buttons: 46 | $(copyButton).prependTo(".hasCopyButton"); 47 | 48 | // Initialize tooltips: 49 | $('.btn-copy-ex').tooltip({container: 'body'}); 50 | 51 | // Initialize clipboard: 52 | var clipboard = new ClipboardJS('[data-clipboard-copy]', { 53 | text: function(trigger) { 54 | return trigger.parentNode.textContent.replace(/\n#>[^\n]*/g, ""); 55 | } 56 | }); 57 | 58 | clipboard.on('success', function(e) { 59 | changeTooltipMessage(e.trigger, 'Copied!'); 60 | e.clearSelection(); 61 | }); 62 | 63 | clipboard.on('error', function(e) { 64 | changeTooltipMessage(e.trigger,'Press Ctrl+C or Command+C to copy'); 65 | }); 66 | 67 | }); 68 | } 69 | 70 | /* Search marking --------------------------*/ 71 | var url = new URL(window.location.href); 72 | var toMark = url.searchParams.get("q"); 73 | var mark = new Mark("main#main"); 74 | if (toMark) { 75 | mark.mark(toMark, { 76 | accuracy: { 77 | value: "complementary", 78 | limiters: [",", ".", ":", "/"], 79 | } 80 | }); 81 | } 82 | 83 | /* Search --------------------------*/ 84 | /* Adapted from https://github.com/rstudio/bookdown/blob/2d692ba4b61f1e466c92e78fd712b0ab08c11d31/inst/resources/bs4_book/bs4_book.js#L25 */ 85 | // Initialise search index on focus 86 | var fuse; 87 | $("#search-input").focus(async function(e) { 88 | if (fuse) { 89 | return; 90 | } 91 | 92 | $(e.target).addClass("loading"); 93 | var response = await fetch($("#search-input").data("search-index")); 94 | var data = await response.json(); 95 | 96 | var options = { 97 | keys: ["what", "text", "code"], 98 | ignoreLocation: true, 99 | threshold: 0.1, 100 | includeMatches: true, 101 | includeScore: true, 102 | }; 103 | fuse = new Fuse(data, options); 104 | 105 | $(e.target).removeClass("loading"); 106 | }); 107 | 108 | // Use algolia autocomplete 109 | var options = { 110 | autoselect: true, 111 | debug: true, 112 | hint: false, 113 | minLength: 2, 114 | }; 115 | var q; 116 | async function searchFuse(query, callback) { 117 | await fuse; 118 | 119 | var items; 120 | if (!fuse) { 121 | items = []; 122 | } else { 123 | q = query; 124 | var results = fuse.search(query, { limit: 20 }); 125 | items = results 126 | .filter((x) => x.score <= 0.75) 127 | .map((x) => x.item); 128 | if (items.length === 0) { 129 | items = [{dir:"Sorry 😿",previous_headings:"",title:"No results found.",what:"No results found.",path:window.location.href}]; 130 | } 131 | } 132 | callback(items); 133 | } 134 | $("#search-input").autocomplete(options, [ 135 | { 136 | name: "content", 137 | source: searchFuse, 138 | templates: { 139 | suggestion: (s) => { 140 | if (s.title == s.what) { 141 | return `${s.dir} >
${s.title}
`; 142 | } else if (s.previous_headings == "") { 143 | return `${s.dir} >
${s.title}
> ${s.what}`; 144 | } else { 145 | return `${s.dir} >
${s.title}
> ${s.previous_headings} > ${s.what}`; 146 | } 147 | }, 148 | }, 149 | }, 150 | ]).on('autocomplete:selected', function(event, s) { 151 | window.location.href = s.path + "?q=" + q + "#" + s.id; 152 | }); 153 | }); 154 | })(window.jQuery || window.$) 155 | 156 | 157 | -------------------------------------------------------------------------------- /vignettes/typed-in-packages.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Using {typed} in packages" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Using {typed} in packages} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | error = TRUE, 13 | collapse = TRUE, 14 | comment = "#>" 15 | ) 16 | ``` 17 | 18 | ```{r setup} 19 | library(typed, warn.conflicts = FALSE) 20 | ``` 21 | 22 | Here are some recommendations to use {typed} in your own package and define 23 | custom types.. 24 | 25 | ## Use {typed} in your package 26 | 27 | Call this to set up your package so you can use 'typed', by editing the DESCRIPTION file 28 | and editing or creating 'R/your.pkg-package.R' 29 | 30 | ```{r, eval = FALSE} 31 | typed::use_typed() 32 | devtools::document() # to actually import from the generated roxygen2 comments 33 | ``` 34 | 35 | ## Use existing types to define your functions 36 | 37 | Here's an example where we define types for the return value, for all arguments, 38 | and for a `msg` variable in the body. 39 | 40 | The use of roxygen2 tags is standard, except that you'll need to make sure to add the `@name` tag. 41 | 42 | ```{r} 43 | #' add_or_subtract 44 | #' 45 | #' @param x double of length 1 46 | #' @param y double of length 1 47 | #' @param subtract whether to subtract instead of adding 48 | #' @export 49 | #' @name add_or_subtract 50 | add_or_subtract <- Double(1) ? function ( 51 | x = ? Double(1), 52 | y = ? Double(1), 53 | subtract = FALSE ? Logical(1, anyNA = FALSE) 54 | ) { 55 | Character(1) ? msg 56 | if(subtract) { 57 | msg <- "subtracting" 58 | message(msg) 59 | return(x - y) 60 | } 61 | msg <- "adding" 62 | message(msg) 63 | x + y 64 | } 65 | ``` 66 | 67 | The created function will be the following: 68 | 69 | ```{r} 70 | add_or_subtract 71 | ``` 72 | 73 | We see that `?` is not present in the 74 | generated code, instead we have `check_arg()`, `declare()`, `check_output()`. 75 | The substitution is done by the first `?`, it is both for efficiency and readability, 76 | unfamiliar users might be intimidated by `?` and calls to `?` don't print nicely 77 | in base R. 78 | 79 | Note that you're free to use these functions directly in your code if you don't 80 | like the `?` Syntax. 81 | 82 | ## Define your own types from existing types 83 | 84 | A way to define custom types is to wrap existing ones and add constraints 85 | 86 | ```{r} 87 | Fruit <- function() { 88 | Character( 89 | length = 1, 90 | ... = "`value` is not a fruit!" ~ . %in% c("apple", "pear", "cherry") 91 | # we could have several args named ... to apply multiple checks 92 | ) 93 | } 94 | 95 | Fruit() ? x <- "potatoe" 96 | ``` 97 | 98 | The type `Any()` is the most general, but in many case it's a good idea to start 99 | from a more restricted type so you get its own checks for free. 100 | 101 | ```{r} 102 | Fruit() ? x <- 1L 103 | ``` 104 | 105 | Here's a case where starting from `Any()` makes sense : 106 | 107 | ```{r} 108 | Ggplot <- function() { 109 | Any(... = "Expected a ggplot object" ~ ggplot2::is.ggplot(value)) 110 | } 111 | 112 | Ggplot() ? x <- 1 113 | ``` 114 | 115 | 116 | A custom type might also just be a restriction on an existing type using existing 117 | arguments. 118 | 119 | In the following example we apply a simple restriction to an existing type, using the existing 120 | argument `length`. 121 | 122 | Here we restrict the length but keep other args flexible by forwarding them: 123 | 124 | ```{r, error = TRUE} 125 | ScalarInteger1 <- function(null_ok = FALSE, ...) { 126 | Integer(length = 1, null_ok = null_ok, ...) 127 | } 128 | ScalarInteger1() ? x <- c(1L, 2L) 129 | ``` 130 | 131 | Here we remove all flexibility: 132 | 133 | ```{r, error = TRUE} 134 | ScalarInteger2 <- function() { 135 | Integer(length = 1) 136 | } 137 | ScalarInteger2() ? x <- c(1L, 2L) 138 | ``` 139 | 140 | 141 | ## Define your own types from scratch 142 | 143 | We can define a check function and use `as_assertion_factory()` on it. 144 | 145 | ```{r, error = TRUE} 146 | check_is_ggplot <- function(x) { 147 | if(!ggplot2::is.ggplot(x)) { 148 | msg <- "Class mismatch" 149 | info1 <- "Expected a ggplot object" 150 | info2 <- sprintf("Got an object of class <%s>", paste(class(x), collapse = "/")) 151 | rlang::abort( 152 | c(msg, i = info1, x = info2) 153 | ) 154 | } 155 | } 156 | Ggplot <- as_assertion_factory(check_is_ggplot) 157 | 158 | Ggplot() ? x <- 1 159 | ``` 160 | 161 | Another example, to impose a data type based on a prototype: 162 | 163 | ```{r} 164 | check_cars <- function(x) vctrs::vec_assert(x, cars) 165 | Cars <- as_assertion_factory(check_cars) 166 | Cars() ? x <- iris 167 | ``` 168 | 169 | Or if we want to be more general: 170 | 171 | ```{r} 172 | check_valid_ptype <- function(x, ptype) vctrs::vec_assert(x, ptype) 173 | Ptype <- as_assertion_factory(check_valid_ptype) 174 | Ptype(cars) ? x <- iris 175 | ``` 176 | 177 | `as_assertion_factory()` 178 | [is actually used to build the native assertion factories of this package](https://github.com/moodymudskipper/typed/blob/master/R/06_native_types.R) 179 | -------------------------------------------------------------------------------- /R/05_new_type.R: -------------------------------------------------------------------------------- 1 | #' Build a new type 2 | #' 3 | #' @param f a function 4 | #' @return a function with class `assertion_factory` 5 | #' 6 | #' @export 7 | as_assertion_factory <- function(f) { 8 | # create a function with arguments being the additional args to f and dots 9 | f_call <- as.call(c(quote(f), quote(value), sapply(names(formals(f)[-1]), as.name))) 10 | 11 | #res <- as.function(c(formals(f)[-1],alist(...=, .extend = NULL), bquote({ 12 | res <- as.function(c(formals(f)[-1],alist(...=), bquote({ 13 | f_call <- substitute(.(f_call)) 14 | # remove if empty 15 | f_call <- Filter(function(value) !identical(value, quote(expr=)), f_call) 16 | 17 | header <- call("{", 18 | quote(f <- .(f)), # so the substituted definition is readable 19 | substitute(value <- F_CALL, list(F_CALL = f_call)) 20 | ) 21 | 22 | # the footer is made of additional assertions derived from `...` 23 | footer <- typed::process_assertion_factory_dots(...) 24 | 25 | # footer2 <- substitute( 26 | # for (assertion in .EXTEND) { 27 | # assertion(value) 28 | # }, 29 | # list(.EXTEND = .extend) 30 | # ) 31 | 32 | if(is.null(footer)) { 33 | # body <- call("{", header, footer2, quote(value)) 34 | body <- call("{", header, quote(value)) 35 | } else { 36 | # body <- call("{", header, footer, footer2, quote(value)) 37 | body <- call("{", header, footer, quote(value)) 38 | } 39 | as.function(c(alist(value=), body), envir = parent.frame()) 40 | }))) 41 | class(res) <- "assertion_factory" 42 | environment(res) <- parent.frame() 43 | res 44 | } 45 | 46 | 47 | #' Process assertion factory dots 48 | #' 49 | #' This needs to be exported, but shouldn't be called by the user 50 | #' 51 | #' @param ... dots 52 | #' @return a `{` expression 53 | #' @export 54 | process_assertion_factory_dots <- function(...) { 55 | args <- list(...) 56 | if(!length(args)) return(NULL) 57 | nms <- allNames(args) 58 | exprs <- vector("list", length(args)) 59 | for (i in seq_along(args)) { 60 | ## is the ith argument named ? 61 | if(!nms[[i]] %in% c("", "...")) { 62 | exprs[[i]] <- bquote( 63 | if(!identical(.(as.name(nms[[i]]))(value), .(args[[i]]))) { 64 | stop(sprintf( 65 | "%s\n%s", 66 | .(paste0("`", nms[[i]], "` mismatch")), 67 | waldo::compare( 68 | .(as.name(nms[[i]]))(value), 69 | .(args[[i]]), 70 | x_arg = .(paste0(nms[[i]], "(value)")), 71 | y_arg = "expected")) 72 | , call. = FALSE) 73 | }) 74 | } else { 75 | ## is it not a formula ? 76 | if(!is.call(args[[i]]) || !identical(args[[i]][[1]], as.name("~"))) { 77 | stop("assertions should be either named function, or unnamed formulas") 78 | } 79 | ## is it a 2 sided formula ? 80 | if (length(args[[i]]) == 3) { 81 | error <- args[[i]][[2]] 82 | assertion <- do.call(substitute, list(args[[i]][[3]], list(. = quote(value)))) 83 | } else { 84 | error <- "mismatch" 85 | assertion <- do.call(substitute, list(args[[i]][[2]], list(. = quote(value)))) 86 | } 87 | 88 | exprs[[i]] <- bquote(if(!.(assertion)) stop( 89 | sprintf( 90 | "%s\n%s", 91 | .(error), 92 | waldo::compare( 93 | FALSE, 94 | TRUE, 95 | x_arg = .(deparse1(assertion)), 96 | y_arg = "expected")) 97 | , call. = FALSE)) 98 | } 99 | } 100 | exprs 101 | as.call(c(quote(`{`), exprs)) 102 | } 103 | 104 | infer_implicit_assignment_call <- function(value) { 105 | # note : attr(, "class") is different from class() 106 | if(is.atomic(value) && is.null(attr(value, "class"))) { 107 | assertion_call <- switch( 108 | typeof(value), 109 | "logical" = quote(Logical()), 110 | "integer" = quote(Integer()), 111 | "double" = quote(Double()), 112 | "complex" = bquote(Any(typeof = "complex")), 113 | "character" = quote(Character()), 114 | "raw" = quote(Raw()) 115 | ) 116 | return(assertion_call) 117 | } 118 | cl <- class(value) 119 | if(length(cl) == 1) { 120 | assertion_call <- switch( 121 | cl, 122 | "list" = quote(List()), 123 | "NULL" = quote(Null()), 124 | "function" = quote(Function()), 125 | "environment" = quote(Environment()), 126 | "name" = quote(Symbol()), 127 | "pairlist" = quote(Pairlist()), 128 | "language" = quote(Language()), 129 | "expression" = quote(Expression()), 130 | "factor" = quote(Factor()), 131 | "data.frame" = quote(Data.frame()), 132 | "matrix" = quote(Matrix()), 133 | "array" = quote(Array()), 134 | "date" = quote(Date()), 135 | "matrix" = quote(Matrix()), 136 | call("Any", class = cl) 137 | ) 138 | return(assertion_call) 139 | } 140 | if (identical(cl, c("POSIXct", "POSIXt"))) { 141 | return(quote(Time())) 142 | } 143 | call("Any", class = cl) 144 | } 145 | 146 | get_assertion <- function(x) { 147 | x <- as.character(substitute(x)) 148 | fun <- activeBindingFunction(x, parent.frame()) 149 | body(fun)[[c(2, 3, 2, 3, 2, 1)]] 150 | } 151 | -------------------------------------------------------------------------------- /docs/bootstrap-toc.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Table of Contents v0.4.1 (http://afeld.github.io/bootstrap-toc/) 3 | * Copyright 2015 Aidan Feldman 4 | * Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */ 5 | (function() { 6 | 'use strict'; 7 | 8 | window.Toc = { 9 | helpers: { 10 | // return all matching elements in the set, or their descendants 11 | findOrFilter: function($el, selector) { 12 | // http://danielnouri.org/notes/2011/03/14/a-jquery-find-that-also-finds-the-root-element/ 13 | // http://stackoverflow.com/a/12731439/358804 14 | var $descendants = $el.find(selector); 15 | return $el.filter(selector).add($descendants).filter(':not([data-toc-skip])'); 16 | }, 17 | 18 | generateUniqueIdBase: function(el) { 19 | var text = $(el).text(); 20 | var anchor = text.trim().toLowerCase().replace(/[^A-Za-z0-9]+/g, '-'); 21 | return anchor || el.tagName.toLowerCase(); 22 | }, 23 | 24 | generateUniqueId: function(el) { 25 | var anchorBase = this.generateUniqueIdBase(el); 26 | for (var i = 0; ; i++) { 27 | var anchor = anchorBase; 28 | if (i > 0) { 29 | // add suffix 30 | anchor += '-' + i; 31 | } 32 | // check if ID already exists 33 | if (!document.getElementById(anchor)) { 34 | return anchor; 35 | } 36 | } 37 | }, 38 | 39 | generateAnchor: function(el) { 40 | if (el.id) { 41 | return el.id; 42 | } else { 43 | var anchor = this.generateUniqueId(el); 44 | el.id = anchor; 45 | return anchor; 46 | } 47 | }, 48 | 49 | createNavList: function() { 50 | return $(''); 51 | }, 52 | 53 | createChildNavList: function($parent) { 54 | var $childList = this.createNavList(); 55 | $parent.append($childList); 56 | return $childList; 57 | }, 58 | 59 | generateNavEl: function(anchor, text) { 60 | var $a = $(''); 61 | $a.attr('href', '#' + anchor); 62 | $a.text(text); 63 | var $li = $('
  • '); 64 | $li.append($a); 65 | return $li; 66 | }, 67 | 68 | generateNavItem: function(headingEl) { 69 | var anchor = this.generateAnchor(headingEl); 70 | var $heading = $(headingEl); 71 | var text = $heading.data('toc-text') || $heading.text(); 72 | return this.generateNavEl(anchor, text); 73 | }, 74 | 75 | // Find the first heading level (`

    `, then `

    `, etc.) that has more than one element. Defaults to 1 (for `

    `). 76 | getTopLevel: function($scope) { 77 | for (var i = 1; i <= 6; i++) { 78 | var $headings = this.findOrFilter($scope, 'h' + i); 79 | if ($headings.length > 1) { 80 | return i; 81 | } 82 | } 83 | 84 | return 1; 85 | }, 86 | 87 | // returns the elements for the top level, and the next below it 88 | getHeadings: function($scope, topLevel) { 89 | var topSelector = 'h' + topLevel; 90 | 91 | var secondaryLevel = topLevel + 1; 92 | var secondarySelector = 'h' + secondaryLevel; 93 | 94 | return this.findOrFilter($scope, topSelector + ',' + secondarySelector); 95 | }, 96 | 97 | getNavLevel: function(el) { 98 | return parseInt(el.tagName.charAt(1), 10); 99 | }, 100 | 101 | populateNav: function($topContext, topLevel, $headings) { 102 | var $context = $topContext; 103 | var $prevNav; 104 | 105 | var helpers = this; 106 | $headings.each(function(i, el) { 107 | var $newNav = helpers.generateNavItem(el); 108 | var navLevel = helpers.getNavLevel(el); 109 | 110 | // determine the proper $context 111 | if (navLevel === topLevel) { 112 | // use top level 113 | $context = $topContext; 114 | } else if ($prevNav && $context === $topContext) { 115 | // create a new level of the tree and switch to it 116 | $context = helpers.createChildNavList($prevNav); 117 | } // else use the current $context 118 | 119 | $context.append($newNav); 120 | 121 | $prevNav = $newNav; 122 | }); 123 | }, 124 | 125 | parseOps: function(arg) { 126 | var opts; 127 | if (arg.jquery) { 128 | opts = { 129 | $nav: arg 130 | }; 131 | } else { 132 | opts = arg; 133 | } 134 | opts.$scope = opts.$scope || $(document.body); 135 | return opts; 136 | } 137 | }, 138 | 139 | // accepts a jQuery object, or an options object 140 | init: function(opts) { 141 | opts = this.helpers.parseOps(opts); 142 | 143 | // ensure that the data attribute is in place for styling 144 | opts.$nav.attr('data-toggle', 'toc'); 145 | 146 | var $topContext = this.helpers.createChildNavList(opts.$nav); 147 | var topLevel = this.helpers.getTopLevel(opts.$scope); 148 | var $headings = this.helpers.getHeadings(opts.$scope, topLevel); 149 | this.helpers.populateNav($topContext, topLevel, $headings); 150 | } 151 | }; 152 | 153 | $(function() { 154 | $('nav[data-toggle="toc"]').each(function(i, el) { 155 | var $nav = $(el); 156 | Toc.init($nav); 157 | }); 158 | }); 159 | })(); 160 | -------------------------------------------------------------------------------- /.github/workflows/commit/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to commit changes to the repository" 2 | inputs: 3 | token: 4 | description: "GitHub token" 5 | required: true 6 | outputs: 7 | sha: 8 | description: "SHA of generated commit" 9 | value: ${{ steps.commit.outputs.sha }} 10 | 11 | runs: 12 | using: "composite" 13 | steps: 14 | - name: Check for changes 15 | id: check 16 | run: | 17 | set -x 18 | if [ -n "$(git status --porcelain)" ]; then 19 | echo "has_changes=true" | tee -a $GITHUB_OUTPUT 20 | else 21 | echo "has_changes=false" | tee -a $GITHUB_OUTPUT 22 | fi 23 | shell: bash 24 | 25 | - name: Determine repository state 26 | if: steps.check.outputs.has_changes == 'true' 27 | id: repo-state 28 | uses: ./.github/workflows/repo-state 29 | 30 | - name: Commit and create PR on protected branch 31 | if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'false' && steps.repo-state.outputs.protected == 'true' 32 | env: 33 | GITHUB_TOKEN: ${{ inputs.token }} 34 | run: | 35 | set -x 36 | current_branch=$(git branch --show-current) 37 | new_branch=gha-commit-$(git rev-parse --short HEAD) 38 | git checkout -b ${new_branch} 39 | git add . 40 | git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" 41 | # Force-push, used in only one place 42 | # Alternative: separate branch names for each usage 43 | git push -u origin HEAD -f 44 | 45 | existing_pr=$(gh pr list --state open --base main --head ${new_branch} --json number --jq '.[] | .number') 46 | if [ -n "${existing_pr}" ]; then 47 | echo "Existing PR: ${existing_pr}" 48 | else 49 | gh pr create --base main --head ${new_branch} --title "chore: Auto-update from GitHub Actions" --body "Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" 50 | fi 51 | 52 | gh workflow run rcc -f ref=$(git rev-parse HEAD) 53 | gh pr merge --merge --auto 54 | shell: bash 55 | 56 | - name: Commit and push on unprotected branch 57 | id: commit 58 | if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'false' && steps.repo-state.outputs.protected == 'false' 59 | env: 60 | GITHUB_TOKEN: ${{ inputs.token }} 61 | run: | 62 | set -x 63 | git fetch 64 | if [ -n "${GITHUB_HEAD_REF}" ]; then 65 | git add . 66 | git stash save 67 | git switch ${GITHUB_HEAD_REF} 68 | git merge origin/${GITHUB_BASE_REF} --no-edit 69 | git stash pop 70 | fi 71 | git add . 72 | git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" 73 | git push -u origin HEAD 74 | 75 | # Only set output if changed 76 | echo sha=$(git rev-parse HEAD) >> $GITHUB_OUTPUT 77 | shell: bash 78 | 79 | - name: Create patch file for foreign branch 80 | if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'true' 81 | run: | 82 | set -x 83 | git diff > changes.patch 84 | echo "Patch file created with uncommitted changes" 85 | cat changes.patch 86 | shell: bash 87 | 88 | - name: Upload patch artifact 89 | if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'true' 90 | uses: actions/upload-artifact@v5 91 | with: 92 | name: changes-patch 93 | path: changes.patch 94 | 95 | - name: Add patch summary on foreign branch 96 | if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'true' 97 | run: | 98 | cat << 'EOF' >> $GITHUB_STEP_SUMMARY 99 | ## Formatting suggestions available 100 | 101 | A patch file with formatting suggestions has been generated. Since this PR is from a forked repository, the changes cannot be pushed automatically. 102 | 103 | You can apply the patch using one of these methods: 104 | 105 | ### Method 1: Apply via gh CLI 106 | 107 | ```bash 108 | # Download and apply the patch directly 109 | gh run download ${{ github.run_id }} --repo ${{ github.repository }} --name changes-patch && git apply changes.patch && rm changes.patch 110 | ``` 111 | 112 | ### Method 2: Download from workflow artifacts 113 | 114 | 1. Download the `changes-patch` artifact from this workflow run 115 | 2. Extract and apply it: 116 | ```bash 117 | git apply changes.patch 118 | ``` 119 | 120 | ### Method 3: View the patch 121 | 122 |
    123 | Click to see the patch contents 124 | 125 | ```diff 126 | EOF 127 | 128 | cat changes.patch >> $GITHUB_STEP_SUMMARY 129 | 130 | cat << 'EOF' >> $GITHUB_STEP_SUMMARY 131 | ``` 132 | 133 |
    134 | EOF 135 | shell: bash 136 | 137 | - name: Fail on foreign branch 138 | if: steps.check.outputs.has_changes == 'true' && steps.repo-state.outputs.foreign == 'true' 139 | run: | 140 | echo "Exiting with failure due to foreign branch. Please apply the patch suggested in the action or PR comment." 141 | exit 1 142 | shell: bash 143 | -------------------------------------------------------------------------------- /man/assertion_factories.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/06_native_types.R 3 | \name{assertion_factories} 4 | \alias{assertion_factories} 5 | \alias{Any} 6 | \alias{Logical} 7 | \alias{Integer} 8 | \alias{Double} 9 | \alias{Character} 10 | \alias{Raw} 11 | \alias{List} 12 | \alias{Null} 13 | \alias{Closure} 14 | \alias{Special} 15 | \alias{Builtin} 16 | \alias{Environment} 17 | \alias{Symbol} 18 | \alias{Pairlist} 19 | \alias{Language} 20 | \alias{Expression} 21 | \alias{Function} 22 | \alias{Factor} 23 | \alias{Data.frame} 24 | \alias{Matrix} 25 | \alias{Array} 26 | \alias{Date} 27 | \alias{Time} 28 | \alias{Dots} 29 | \title{Assertion factories of package 'typed'} 30 | \usage{ 31 | Any(length = NULL, ...) 32 | 33 | Logical(length = NULL, null_ok = FALSE, ...) 34 | 35 | Integer(length = NULL, null_ok = FALSE, ...) 36 | 37 | Double(length = NULL, null_ok = FALSE, ...) 38 | 39 | Character(length = NULL, null_ok = FALSE, ...) 40 | 41 | Raw(length = NULL, null_ok = FALSE, ...) 42 | 43 | List(length = NULL, each, data_frame_ok, null_ok = FALSE, ...) 44 | 45 | Null(...) 46 | 47 | Closure(null_ok = FALSE, ...) 48 | 49 | Special(null_ok = FALSE, ...) 50 | 51 | Builtin(null_ok = FALSE, ...) 52 | 53 | Environment(null_ok = FALSE, ...) 54 | 55 | Symbol(null_ok = FALSE, ...) 56 | 57 | Pairlist(length = NULL, each, null_ok = TRUE, ...) 58 | 59 | Language(null_ok = FALSE, ...) 60 | 61 | Expression(length = NULL, null_ok = FALSE, ...) 62 | 63 | Function(null_ok = FALSE, ...) 64 | 65 | Factor(length = NULL, levels, null_ok = FALSE, ...) 66 | 67 | Matrix(nrow, ncol, null_ok = FALSE, ...) 68 | 69 | Array(dim, null_ok = FALSE, ...) 70 | 71 | Data.frame(nrow, ncol, each, null_ok = FALSE, ...) 72 | 73 | Date(length = NULL, null_ok = FALSE, ...) 74 | 75 | Time(length = NULL, null_ok = FALSE, ...) 76 | 77 | Dots(length = NULL, each, ...) 78 | 79 | Logical(length = NULL, null_ok = FALSE, ...) 80 | 81 | Integer(length = NULL, null_ok = FALSE, ...) 82 | 83 | Double(length = NULL, null_ok = FALSE, ...) 84 | 85 | Character(length = NULL, null_ok = FALSE, ...) 86 | 87 | Raw(length = NULL, null_ok = FALSE, ...) 88 | 89 | List(length = NULL, each, data_frame_ok = TRUE, null_ok = FALSE, ...) 90 | 91 | Null(...) 92 | 93 | Closure(null_ok = FALSE, ...) 94 | 95 | Special(null_ok = FALSE, ...) 96 | 97 | Builtin(null_ok = FALSE, ...) 98 | 99 | Environment(null_ok = FALSE, ...) 100 | 101 | Symbol(null_ok = FALSE, ...) 102 | 103 | Pairlist(length = NULL, each, null_ok = TRUE, ...) 104 | 105 | Language(null_ok = FALSE, ...) 106 | 107 | Expression(length = NULL, null_ok = FALSE, ...) 108 | 109 | Function(null_ok = FALSE, ...) 110 | 111 | Factor(length = NULL, levels, null_ok = FALSE, ...) 112 | 113 | Data.frame(nrow, ncol, each, null_ok = FALSE, ...) 114 | 115 | Matrix(nrow, ncol, null_ok = FALSE, ...) 116 | 117 | Array(dim, null_ok = FALSE, ...) 118 | 119 | Date(length = NULL, null_ok = FALSE, ...) 120 | 121 | Time(length = NULL, null_ok = FALSE, ...) 122 | 123 | Dots(length = NULL, each, ...) 124 | } 125 | \arguments{ 126 | \item{length}{length of the object} 127 | 128 | \item{...}{additional conditions, see details.} 129 | 130 | \item{null_ok}{whether \code{NULL} values should be accepted, and not subjected to 131 | any further check.} 132 | 133 | \item{each}{assertion that every item must satisfy} 134 | 135 | \item{data_frame_ok}{whether data frames are to be considered as lists} 136 | 137 | \item{levels}{factor levels} 138 | 139 | \item{nrow}{number of rows} 140 | 141 | \item{ncol}{number of columns} 142 | 143 | \item{dim}{dimensions} 144 | } 145 | \value{ 146 | A function, and more specifically, an assertion as defined above. 147 | } 148 | \description{ 149 | These functions are assertion factories, they produce assertions, 150 | which take an object, check conditions, and 151 | returns the input, usually unmodified (never modified with the functions 152 | documented on this page). 153 | 154 | Additional conditions can be provided : 155 | \itemize{ 156 | \item If they are named, the name should be the name of a function to use on our 157 | object, and the value should be the expected value. 158 | \item If they are unnamed, they should be formulas, the right hand side should 159 | be a condition, using \code{value} or \code{.} as a placeholder for the latter, and 160 | the optional \code{lhs} an error message. 161 | } 162 | 163 | \code{Any} is the most general assertion factory, it doesn't check anything unless 164 | provided additional conditions through \code{...}. Others use the base \verb{is.} function 165 | if available, or check that the object is of the relevant type with \code{typeof} 166 | for atomic types, or check that the class of the checked value contains 167 | the relevant class. 168 | 169 | See advanced examples at the bottom, including uses of \code{Symbol()} and \code{Dots()}. 170 | } 171 | \examples{ 172 | 173 | \dontrun{ 174 | # fails 175 | Integer() ? x <- 1 176 | # equivalent to 177 | declare("x", Integer(), value = 1) 178 | 179 | Integer(2) ? x <- 1L 180 | 181 | # we can use additional conditions in `...` 182 | Integer(anyNA = FALSE) ? x <- c(1L, NA, 1L) 183 | Integer(anyDuplicated = 0L) ? x <- c(1L, NA, 1L) 184 | } 185 | 186 | Integer(2) ? x <- 11:12 187 | 188 | \dontrun{ 189 | # We can also use it directly to test assertions 190 | Integer() ? x <- 1 191 | # equivalent to 192 | declare("x", Integer(), value = 1) 193 | 194 | Integer(2) ? x <- 1L 195 | } 196 | 197 | \dontrun{ 198 | # I we want to restrict the quoted expression rather than the value of an 199 | # argument, we can use `?~` : 200 | identity_sym_only <- ? function (x= ?~ Symbol()) { 201 | x 202 | } 203 | 204 | a <- 1 205 | identity_sym_only(a) 206 | identity_sym_only(a + a) 207 | 208 | identity_sym_only 209 | } 210 | 211 | \dontrun{ 212 | integer_list <- ? function (...= ? Integer()) { 213 | list(...) 214 | } 215 | 216 | integer_list(1L, 2L, "a") 217 | 218 | integer_pair <- ? function (...= ? Dots(2, each = Integer())) { 219 | list(...) 220 | } 221 | 222 | integer_pair(1L, 2L, 3L) 223 | integer_pair(1L, "a", "a") 224 | 225 | x <- 1 226 | y <- 2 227 | symbol_list1 <- ? function (...= ? Dots(2, Symbol())) { 228 | list(...) 229 | } 230 | symbol_list1(quote(x), quote(y)) 231 | symbol_list1(x, y) 232 | 233 | symbol_list2 <- ? function (...= ?~ Dots(2, Symbol())) { 234 | list(...) 235 | } 236 | symbol_list2(x, x + y) 237 | symbol_list2(x, y) 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /docs/reference/get_assertion.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Fetch assertion from active binding function — get_assertion • typed 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
    71 |
    72 | 111 | 112 | 113 | 114 |
    115 | 116 |
    117 |
    118 | 123 | 124 |
    125 |

    Fetch assertion from active binding function

    126 |
    127 | 128 |
    get_assertion(x)
    129 | 130 |

    Arguments

    131 | 132 | 133 | 134 | 135 | 136 | 137 |
    x

    obect

    138 | 139 | 140 |
    141 | 146 |
    147 | 148 | 149 |
    150 | 153 | 154 |
    155 |

    Site built with pkgdown 1.5.1.

    156 |
    157 | 158 |
    159 |
    160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /.github/workflows/install/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to run for installing R packages" 2 | inputs: 3 | token: 4 | description: GitHub token, set to secrets.GITHUB_TOKEN 5 | required: true 6 | r-version: 7 | description: Passed on to r-lib/actions/setup-r@v2 8 | required: false 9 | default: release 10 | install-r: 11 | description: Passed on to r-lib/actions/setup-r@v2 12 | required: false 13 | default: true 14 | needs: 15 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 16 | required: false 17 | default: "" 18 | packages: 19 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 20 | required: false 21 | default: deps::., any::sessioninfo 22 | extra-packages: 23 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 24 | required: false 25 | default: any::rcmdcheck 26 | cache-version: 27 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 28 | required: false 29 | default: 1 30 | 31 | runs: 32 | using: "composite" 33 | steps: 34 | - name: Set environment variables 35 | run: | 36 | echo "R_REMOTES_NO_ERRORS_FROM_WARNINGS=true" | tee -a $GITHUB_ENV 37 | echo "R_KEEP_PKG_SOURCE=yes" | tee -a $GITHUB_ENV 38 | echo "_R_CHECK_SYSTEM_CLOCK_=false" | tee -a $GITHUB_ENV 39 | echo "_R_CHECK_FUTURE_FILE_TIMESTAMPS_=false" | tee -a $GITHUB_ENV 40 | # prevent rgl issues because no X11 display is available 41 | echo "RGL_USE_NULL=true" | tee -a $GITHUB_ENV 42 | # from https://github.com/r-devel/r-dev-web/blob/main/CRAN/QA/Kurt/lib/R/Scripts/check_CRAN_incoming.R 43 | echo "_R_CHECK_CRAN_INCOMING_CHECK_FILE_URIS_=true" | tee -a $GITHUB_ENV 44 | echo "_R_CHECK_CRAN_INCOMING_NOTE_GNU_MAKE_=true" | tee -a $GITHUB_ENV 45 | echo "_R_CHECK_PACKAGE_DEPENDS_IGNORE_MISSING_ENHANCES_=true" | tee -a $GITHUB_ENV 46 | echo "_R_CHECK_CODE_CLASS_IS_STRING_=true" | tee -a $GITHUB_ENV 47 | echo "_R_CHECK_CODOC_VARIABLES_IN_USAGES_=true" | tee -a $GITHUB_ENV 48 | echo "_R_CHECK_CONNECTIONS_LEFT_OPEN_=true" | tee -a $GITHUB_ENV 49 | echo "_R_CHECK_DATALIST_=true" | tee -a $GITHUB_ENV 50 | echo "_R_CHECK_NEWS_IN_PLAIN_TEXT_=true" | tee -a $GITHUB_ENV 51 | echo "_R_CHECK_PACKAGES_USED_CRAN_INCOMING_NOTES_=true" | tee -a $GITHUB_ENV 52 | echo "_R_CHECK_RD_CONTENTS_KEYWORDS_=true" | tee -a $GITHUB_ENV 53 | echo "_R_CHECK_R_DEPENDS_=warn" | tee -a $GITHUB_ENV 54 | echo "_R_CHECK_S3_METHODS_SHOW_POSSIBLE_ISSUES_=true" | tee -a $GITHUB_ENV 55 | echo "_R_CHECK_THINGS_IN_TEMP_DIR_=true" | tee -a $GITHUB_ENV 56 | echo "_R_CHECK_UNDOC_USE_ALL_NAMES_=true" | tee -a $GITHUB_ENV 57 | echo "_R_CHECK_URLS_SHOW_301_STATUS_=true" | tee -a $GITHUB_ENV 58 | echo "_R_CXX_USE_NO_REMAP_=true" | tee -a $GITHUB_ENV 59 | # There is no way to disable recency and frequency checks when the incoming checks are run 60 | # echo "_R_CHECK_CRAN_INCOMING_=true" | tee -a $GITHUB_ENV 61 | echo "_R_CHECK_CRAN_INCOMING_SKIP_LARGE_VERSION_=true" | tee -a $GITHUB_ENV 62 | echo "_R_CHECK_FORCE_SUGGESTS_=false" | tee -a $GITHUB_ENV 63 | shell: bash 64 | 65 | - name: Set environment variables (non-macOS only) 66 | if: runner.os != 'macOS' 67 | run: | 68 | echo "_R_CHECK_THINGS_IN_OTHER_DIRS_=true" | tee -a $GITHUB_ENV 69 | shell: bash 70 | 71 | - name: Set environment variables (non-Windows only) 72 | if: runner.os != 'Windows' 73 | run: | 74 | echo "_R_CHECK_BASHISMS_=true" | tee -a $GITHUB_ENV 75 | shell: bash 76 | 77 | - name: Review environment variables 78 | run: | 79 | set -x 80 | env | sort 81 | shell: bash 82 | 83 | - name: Update apt 84 | if: runner.os == 'Linux' 85 | run: | 86 | sudo apt-get update 87 | sudo apt-get install -y aspell 88 | echo "_R_CHECK_CRAN_INCOMING_USE_ASPELL_=true" | tee -a $GITHUB_ENV 89 | shell: bash 90 | 91 | - name: Remove pkg-config@0.29.2 92 | if: runner.os == 'macOS' 93 | run: | 94 | brew uninstall pkg-config@0.29.2 || true 95 | shell: bash 96 | 97 | - uses: r-lib/actions/setup-pandoc@v2 98 | 99 | - uses: r-lib/actions/setup-r@v2 100 | with: 101 | r-version: ${{ inputs.r-version }} 102 | install-r: ${{ inputs.install-r }} 103 | http-user-agent: ${{ matrix.config.http-user-agent }} 104 | use-public-rspm: true 105 | 106 | - id: get-extra 107 | run: | 108 | set -x 109 | packages=$( ( grep Config/gha/extra-packages DESCRIPTION || true ) | cut -d " " -f 2-) 110 | echo packages=$packages >> $GITHUB_OUTPUT 111 | shell: bash 112 | 113 | - uses: r-lib/actions/setup-r-dependencies@v2 114 | env: 115 | GITHUB_PAT: ${{ inputs.token }} 116 | with: 117 | pak-version: stable 118 | needs: ${{ inputs.needs }} 119 | packages: ${{ inputs.packages }} 120 | extra-packages: ${{ inputs.extra-packages }} ${{ ( matrix.covr && 'r-lib/covr xml2' ) || '' }} ${{ steps.get-extra.outputs.packages }} 121 | cache-version: ${{ inputs.cache-version }} 122 | 123 | - name: Add pkg.lock to .gitignore 124 | run: | 125 | set -x 126 | if ! [ -f .github/.gitignore ] || [ -z "$(grep '^/pkg.lock$' .github/.gitignore)" ]; then 127 | echo /pkg.lock >> .github/.gitignore 128 | fi 129 | shell: bash 130 | 131 | - name: Add fake qpdf and checkbashisms 132 | if: runner.os == 'Linux' 133 | run: | 134 | sudo ln -s $(which true) /usr/local/bin/qpdf 135 | sudo ln -s $(which true) /usr/local/bin/checkbashisms 136 | shell: bash 137 | 138 | - name: Install ccache 139 | uses: krlmlr/ccache-action@parallel-dir 140 | with: 141 | max-size: 10G 142 | verbose: 1 143 | save: false 144 | restore: false 145 | 146 | - name: Use ccache for compiling R code, and parallelize 147 | run: | 148 | mkdir -p ~/.R 149 | echo 'CC := ccache $(CC)' >> ~/.R/Makevars 150 | echo 'CXX := ccache $(CXX)' >> ~/.R/Makevars 151 | echo 'CXX11 := ccache $(CXX11)' >> ~/.R/Makevars 152 | echo 'CXX14 := ccache $(CXX14)' >> ~/.R/Makevars 153 | echo 'CXX17 := ccache $(CXX17)' >> ~/.R/Makevars 154 | echo 'MAKEFLAGS = -j2' >> ~/.R/Makevars 155 | cat ~/.R/Makevars 156 | 157 | echo 'CCACHE_SLOPPINESS=locale,time_macros' | tee -a $GITHUB_ENV 158 | 159 | # echo 'CCACHE_DEBUG=true' | tee -a $GITHUB_ENV 160 | # echo "CCACHE_DEBUGDIR=$(dirname $(pwd))/ccache-debug" | tee -a $GITHUB_ENV 161 | # mkdir -p $(dirname $(pwd))/.ccache-debug 162 | 163 | echo 'PKG_BUILD_EXTRA_FLAGS=false' | tee -a $GITHUB_ENV 164 | 165 | # Repair 166 | git rm -rf .ccache || true 167 | rm -rf .ccache 168 | shell: bash 169 | 170 | - name: Show R CMD config --all 171 | run: | 172 | R CMD config --all 173 | shell: bash 174 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Page not found (404) • typed 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | Skip to contents 28 | 29 | 30 |
    70 |
    71 |
    75 | 76 | Content not found. Please use links in the navbar. 77 | 78 |
    79 |
    80 | 81 | 82 |
    85 | 86 | 89 | 90 |
    91 |
    92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /docs/authors.html: -------------------------------------------------------------------------------- 1 | 2 | Authors and Citation • typed 6 | Skip to contents 7 | 8 | 9 |
    44 |
    45 |
    48 | 49 |
    50 |

    Authors

    51 | 52 |
    • 53 |

      Antoine Fabri. Author, maintainer. 54 |

      55 |
    • 56 |
    57 | 58 |
    59 |

    Citation

    60 |

    Source: DESCRIPTION

    61 | 62 |

    Fabri A (2025). 63 | typed: Support Types for Variables, Arguments, and Return Values. 64 | R package version 0.0.1.9000, https://moodymudskipper.github.io/typed/, https://github.com/moodymudskipper/typed. 65 |

    66 |
    @Manual{,
    67 |   title = {typed: Support Types for Variables, Arguments, and Return Values},
    68 |   author = {Antoine Fabri},
    69 |   year = {2025},
    70 |   note = {R package version 0.0.1.9000, https://moodymudskipper.github.io/typed/},
    71 |   url = {https://github.com/moodymudskipper/typed},
    72 | }
    73 |
    74 |
    76 | 77 | 78 |
    81 | 82 | 85 | 86 |
    87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /docs/reference/as_assertion_factory.html: -------------------------------------------------------------------------------- 1 | 2 | Build a new type — as_assertion_factory • typed 6 | Skip to contents 7 | 8 | 9 |
    44 |
    45 |
    50 | 51 |
    52 |

    Build a new type

    53 |
    54 | 55 |
    56 |

    Usage

    57 |
    as_assertion_factory(f)
    58 |
    59 | 60 |
    61 |

    Arguments

    62 |
    f
    63 |

    a function

    64 | 65 |
    66 |
    67 |

    Value

    68 | 69 | 70 |

    a function with class assertion_factory

    71 | 72 | 73 |
    74 | 75 |
    77 | 78 | 79 |
    82 | 83 | 86 | 87 |
    88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /docs/reference/process_assertion_factory_dots.html: -------------------------------------------------------------------------------- 1 | 2 | Process assertion factory dots — process_assertion_factory_dots • typed 6 | Skip to contents 7 | 8 | 9 |
    44 |
    45 |
    50 | 51 |
    52 |

    This needs to be exported, but shouldn't be called by the user

    53 |
    54 | 55 |
    56 |

    Usage

    57 |
    process_assertion_factory_dots(...)
    58 |
    59 | 60 |
    61 |

    Arguments

    62 |
    ...
    63 |

    dots

    64 | 65 |
    66 |
    67 |

    Value

    68 | 69 | 70 |

    a { expression

    71 |
    72 | 73 |
    75 | 76 | 77 |
    80 | 81 | 84 | 85 |
    86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /R/02_declare.R: -------------------------------------------------------------------------------- 1 | #' @param .output function output 2 | #' @param .assertion an assertion 3 | #' @param ... additional arguments passed to assertion 4 | #' 5 | #' @export 6 | #' @return `.output`if it satisfies the assertion, fails otherwise. 7 | #' @rdname check_arg 8 | check_output <- function(.output, .assertion, ...) { 9 | val <- try(.assertion(.output, ...), silent = TRUE) 10 | if(inherits(val, "try-error")) { 11 | e <- attr(val, "condition")$message 12 | check_arg_call <- deparse1(sys.call()) 13 | fun_call <- deparse1(sys.call(-1)) 14 | e <- sprintf("In `%s` at `%s`:\nwrong return value, %s", fun_call, check_arg_call, e) 15 | stop(e, call. = FALSE) 16 | } 17 | .output 18 | } 19 | 20 | #' Check Argument Types and Return Type 21 | #' 22 | #' These functions are not designed to be used directly, we advise to use the 23 | #' syntaxes described in `?declare` instead. `check_arg` checks that arguments 24 | #' satisfy an assertion, and if relevant make them into active bindings to make sure they 25 | #' always satisy it. `check_output` checks that the value, presumably a return 26 | #' value, satisfies an assertion, 27 | #' 28 | #' @param .arg function argument 29 | #' @param .assertion an assertion 30 | #' @param ... additional arguments passed to assertion 31 | #' @param .bind whether to actively bind the argument so it cannot be modified 32 | #' unless it satisfies the assertion 33 | #' @return returns `NULL` invisibly, called for side effects. 34 | #' @export 35 | check_arg <- function(.arg, .assertion, ..., .bind = FALSE) { 36 | if(.bind) { 37 | var_nm <- as.character(substitute(.arg)) 38 | assertion_quoted <- substitute(.assertion) 39 | pf <- parent.frame() 40 | dots <- eval(substitute(alist(...))) 41 | f <- eval(substitute( 42 | local({ 43 | val <- ARG 44 | # use long name`assigned_value` so trace is more intuitive 45 | function(assigned_value) { 46 | # browser() 47 | if (!missing(assigned_value)) { 48 | # we should catch this error and use `sys.call()` to enrich it 49 | val <<- try(ASSERTION_CALL, silent = TRUE) 50 | if (inherits(val, "try-error")) { 51 | e <- attr(val, "condition")$message 52 | fun_call <- sys.call(-1) 53 | if(!is.null(fun_call)) { 54 | fun_call <- deparse1(fun_call) 55 | e <- sprintf("In `%s` at `%s <- ...`:\n%s", fun_call, VAR_NM, e) 56 | } 57 | stop(e, call. = FALSE) 58 | } 59 | } 60 | val 61 | } 62 | }, envir = pf), 63 | list( 64 | ASSERTION_CALL = as.call(c(assertion_quoted, quote(assigned_value), dots)), 65 | VAR_NM = var_nm, 66 | ARG = .arg 67 | ) 68 | )) 69 | attr(f, "srcref") <- NULL # so it's not set to old definition* 70 | rm(list = var_nm, envir = pf) 71 | makeActiveBinding(var_nm, f, pf) 72 | } 73 | 74 | val <- tryCatch(.assertion(.arg, ...), error = function(e) e) 75 | if(class(val)[[1]] == "simpleError") { # faster than inherits 76 | e <- val$message 77 | check_arg_call <- deparse1(sys.call()) 78 | fun_call <- deparse1(sys.call(-1)) 79 | e <- sprintf("In `%s` at `%s`:\nwrong argument to function, %s", fun_call, check_arg_call, e) 80 | stop(e, call. = FALSE) 81 | } 82 | 83 | invisible(NULL) 84 | } 85 | 86 | #' @param x variable name as a string 87 | #' @param assertion a function 88 | #' @param value an optional value 89 | #' @param const whether to declare `x` as a constant 90 | #' 91 | #' @export 92 | declare <- function(x, assertion, value, const = FALSE) { 93 | pf <- parent.frame() 94 | f_env <- new.env(parent = pf) 95 | 96 | if(missing(assertion)) { 97 | assertion_quoted <- infer_implicit_assignment_call(value) 98 | assertion <- eval(assertion_quoted) 99 | } else { 100 | ## is assertion a assertion_ factory ? 101 | assertion_quoted <- substitute(assertion) 102 | if(inherits(assertion, "assertion_factory")) { 103 | ## overwrite it with a call to itself with no arg 104 | # this is so we can use `Integer` in place of `Integer()` for instance 105 | assertion <- assertion() 106 | assertion_quoted <- as.call(list(assertion_quoted)) 107 | } 108 | } 109 | 110 | ## is value provided ? 111 | if(!missing(value)) { 112 | val <- try(assertion(value), silent = TRUE) 113 | if(inherits(val, "try-error")) { 114 | e <- attr(val, "condition")$message 115 | declare_call <- sys.call() 116 | 117 | sc4 <- if(length(sys.calls()) >=4) sys.call(-4) # the potential `?` call 118 | if (is.call(sc4) && identical(sc4[[1]], quote(`?`))) { 119 | declare_call <- sc4 120 | fun_call <- sys.call(-5) 121 | declare_call <- paste(deparse1(declare_call[[2]]),"?", deparse1(declare_call[[3]])) 122 | } else { 123 | fun_call <- sys.call(-1) 124 | declare_call <- deparse1(declare_call) 125 | } 126 | 127 | # if we use reprex or knitr the call stack is edited, we account for it 128 | is_eval_call <- 129 | is.call(fun_call) && identical(fun_call[[1]], quote(eval)) # nocov 130 | 131 | if(!is.null(fun_call) && !is_eval_call) { 132 | fun_call <- deparse1(fun_call) 133 | e <- sprintf("In `%s` at `%s`:\n%s", fun_call, declare_call, e) 134 | } 135 | stop(e, call. = FALSE) 136 | } 137 | value <- val 138 | } else { 139 | # NULL is accepted as a first value even if it doesn't pass the check 140 | # this is because we're very flexible with types, the alternative would be 141 | # to force the user to define a default object when value is missing in the assertion factory 142 | value <- NULL 143 | } 144 | 145 | if(const) { 146 | # we could use `lockBinding()` but we'd lose flexibility on the error message 147 | # so we use an active binding here as well 148 | f <- eval(substitute( 149 | local( 150 | { 151 | val <- VALUE 152 | function(assigned_value) { 153 | if (!missing(assigned_value)) { 154 | e <- paste0("Can't assign to a constant") 155 | fun_call <- sys.call(-1) 156 | is_eval_call <- 157 | is.call(fun_call) && identical(fun_call[[1]], quote(eval)) # nocov 158 | if(!is.null(fun_call) && !is_eval_call) { 159 | fun_call <- deparse1(fun_call) 160 | e <- sprintf("In `%s` at `%s <- ...`:\n%s", fun_call, VAR_NM, e) 161 | } 162 | stop(e, call. = FALSE) 163 | } 164 | val 165 | } 166 | }, 167 | envir = f_env), 168 | list(VAR_NM = x, VALUE = value) 169 | )) 170 | } else { 171 | f <- eval( 172 | substitute( 173 | local({ 174 | val <- VALUE 175 | # use long name`assigned_value` so trace is more intuitive 176 | function(assigned_value) { 177 | if (!missing(assigned_value)) { 178 | # we should catch this error and use `sys.call()` to enrich it 179 | val <<- try(ASSERTION(assigned_value), silent = TRUE) 180 | if(inherits(val, "try-error")) { 181 | e <- attr(val, "condition")$message 182 | fun_call <- sys.call(-1) 183 | is_eval_call <- 184 | is.call(fun_call) && identical(fun_call[[1]], quote(eval)) # nocov 185 | if(!is.null(fun_call) && !is_eval_call) { 186 | fun_call <- deparse1(fun_call) 187 | e <- sprintf("In `%s` at `%s <- ...`:\n%s", fun_call, VAR_NM, e) 188 | } 189 | stop(e, call. = FALSE) 190 | } 191 | } 192 | val 193 | } 194 | }, envir = f_env), 195 | list (ASSERTION = assertion_quoted, VAR_NM = x, VALUE = value) 196 | ) 197 | ) 198 | } 199 | 200 | attr(f, "srcref") <- NULL # so it's not set to old definition 201 | if (exists(x, pf)) { 202 | rm(list =x, envir = pf) 203 | } 204 | makeActiveBinding(x, f, pf) 205 | 206 | return(invisible(value)) 207 | } 208 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | [![Travis build status](https://travis-ci.com/moodymudskipper/typed.svg?branch=iteration2)](https://travis-ci.com/moodymudskipper/typed) 7 | [![Codecov test coverage](https://codecov.io/gh/moodymudskipper/typed/branch/master/graph/badge.svg)](https://codecov.io/gh/moodymudskipper/typed?branch=master) 8 | 9 | 10 | 11 | ```{r, include = FALSE} 12 | knitr::opts_chunk$set( 13 | error = TRUE, 14 | collapse = TRUE, 15 | comment = "#>", 16 | fig.path = "man/figures/README-", 17 | out.width = "100%" 18 | ) 19 | ``` 20 | 21 | # typed 22 | 23 | *{typed}* implements a type system for R, it has 3 main features: 24 | 25 | * set variable types in a script or the body of a function, so they can't be 26 | assigned illegal values 27 | * set argument types in a function definition 28 | * set return type of a function 29 | 30 | The user can define their own types, or leverage assertions from 31 | other packages. 32 | 33 | Under the hood variable types use active bindings, so once a variable is restricted 34 | by an assertion, it cannot be modified in a way that would 35 | not satisfy it. 36 | 37 | ## Installation 38 | 39 | Install CRAN version with: 40 | 41 | ``` r 42 | install.packages("typed") 43 | ``` 44 | 45 | or development version with : 46 | 47 | ``` r 48 | remotes::install_github("moodymudskipper/typed") 49 | ``` 50 | 51 | And attach with : 52 | 53 | ```{r} 54 | # masking warning about overriding `?` 55 | library(typed, warn.conflicts = FALSE) 56 | ``` 57 | 58 | ## Set variable type 59 | 60 | ### Question mark notation and `declare` 61 | 62 | Here are examples on how we would set types 63 | 64 | ```{r} 65 | Character() ? x # restrict x to "character" type 66 | x <- "a" 67 | x 68 | 69 | Integer(3) ? y <- 1:3 # restrict y to "integer" type of length 3 70 | y 71 | ``` 72 | 73 | We cannot assign values of the wrong type to `x` and `y` anymore. 74 | 75 | ```{r} 76 | x <- 2 77 | 78 | y <- 4:5 79 | ``` 80 | 81 | But the right type will work. 82 | 83 | ```{r} 84 | x <- c("b", "c") 85 | 86 | y <- c(1L, 10L, 100L) 87 | ``` 88 | 89 | `declare` is a strict equivalent, slightly more efficient, which looks like `base::assign`. 90 | 91 | ```{r} 92 | declare("x", Character()) 93 | x <- "a" 94 | x 95 | 96 | declare("y", Integer(3), 1:3) 97 | y 98 | ``` 99 | 100 | ### Assertion factories and assertions 101 | 102 | `Integer` and `Character` are function factories (functions that return functions), 103 | thus `Integer(3)` and `Character()` are functions. 104 | 105 | The latter functions operate checks on a value and in case of success 106 | return this value, generally unmodified. For instance : 107 | 108 | ```{r} 109 | Integer(3)(1:2) 110 | 111 | Character()(3) 112 | ``` 113 | 114 | We call `Integer(3)` and `Character()` assertions, and we call 115 | `Integer` and `Character` assertion factories (or just types, with then we must 116 | be careful not to confuse them with atomic types returned by the `typeof` function). 117 | 118 | The package contains many assertion factories (see `?assertion_factories`), 119 | the main ones are: 120 | 121 | * `Any` (No default restriction) 122 | * `Logical` 123 | * `Integer` 124 | * `Double` 125 | * `Character` 126 | * `List` 127 | * `Environment` 128 | * `Factor` 129 | * `Matrix` 130 | * `Data.frame` 131 | * `Date` 132 | * `Time` (POSIXct) 133 | 134 | ### Advanced type restriction using arguments 135 | 136 | As we've seen with `Integer(3)`, passing arguments to a 137 | assertion factory restricts the type. 138 | 139 | For instance `Integer` has arguments `length` `null_ok` and 140 | `...`. We already used `length`, `null_ok` is convenient to allow a default `NULL` 141 | value in addition to the `"integer"` type. 142 | 143 | The arguments can differ between assertion factories, for instance `Data.frame` 144 | has `nrow`, `ncol`, `each`, `null_ok` and `...` 145 | 146 | ```{r} 147 | Data.frame() ? x <- iris 148 | Data.frame(ncol = 2) ? x <- iris 149 | Data.frame(each = Double()) ? x <- iris 150 | ``` 151 | 152 | In the dots we can use arguments named as functions and with the value of 153 | the expected result. 154 | 155 | ```{r} 156 | # Integer has no anyNA arg but we can still use it because a function named 157 | # this way exists 158 | Integer(anyNA = FALSE) ? x <- c(1L, 2L, NA) 159 | ``` 160 | 161 | Useful arguments might be for instance, `anyDuplicated = 0L`, `names = NULL`, 162 | `attributes = NULL`... Any available function can be used. 163 | 164 | That makes assertion factories very flexible! If it is still not flexible enough, 165 | we can provide arguments arguments named `...` to functional factories to add 166 | a custom restriction, this is usually better done by defining a wrapper. 167 | 168 | ```{r} 169 | Character(1, ... = "`value` is not a fruit!" ~ . %in% c("apple", "pear", "cherry")) ? 170 | x <- "potatoe" 171 | ``` 172 | 173 | This is often better done by defining a wrapper as shown below. 174 | 175 | ### Constants 176 | 177 | To define a constant, we just surround the variable by parentheses (think of 178 | them as a protection) 179 | 180 | ```{r} 181 | Double() ? (x) <- 1 182 | x <- 2 183 | 184 | # defining a type is optional 185 | ? (y) <- 1 186 | y <- 2 187 | ``` 188 | 189 | ### Set a function's argument type 190 | 191 | We can set argument types this way : 192 | 193 | 194 | ```{r} 195 | add <- ? function (x= ? Double(), y= 1 ? Double()) { 196 | x + y 197 | } 198 | ``` 199 | 200 | Note that we started the definition with a `?`, and that we gave a default to 201 | `y`, but not `x`. Note also the `=` sign next to `x`, necessary even when we 202 | have no default value. If you forget it you'll have an error "unexpected `?` in ...". 203 | 204 | This created the following function, by adding checks at the top of the 205 | body 206 | 207 | ```{r} 208 | add 209 | ``` 210 | 211 | Let's test it by providing a right and wrong type. 212 | 213 | ```{r} 214 | add(2, 3) 215 | add(2, 3L) 216 | ``` 217 | 218 | If we want to restrict `x` and `y` to the type "integer" in the rest of 219 | the body, so they cannot be overwritten by character for instance,we can use 220 | the `?+` notation : 221 | 222 | ```{r} 223 | add <- ? function (x= ?+ Double(), y= 1 ?+ Double()) { 224 | x + y 225 | } 226 | 227 | add 228 | ``` 229 | 230 | We see that it is translated into a `check_arg` call containing a `.bind = TRUE` 231 | argument. 232 | 233 | ## Set a function's return type 234 | 235 | To set a return type we use `?` before the function definition as in the previous section, 236 | but we type an assertion on the left hand side. 237 | 238 | ```{r} 239 | add_or_subtract <- Double() ? function (x, y, subtract = FALSE) { 240 | if(subtract) return(x - y) 241 | x + y 242 | } 243 | add_or_subtract 244 | ``` 245 | 246 | We see that the returned values have been wrapped inside `check_output` calls. 247 | 248 | # Use type in a package and define your own types 249 | 250 | See `vignette("typed-in-packaged", "typed")` or the Article section if you're 251 | browsing the pkgdown website. 252 | 253 | ## Acknowledgements 254 | 255 | This is inspired in good part by Jim Hester and Gabor Csardi's work and many 256 | great efforts on static typing, assertions, or annotations in R, in particular: 257 | 258 | * Gabor Csardy's [*argufy*](https://github.com/gaborcsardi/argufy) 259 | * Richie Cotton's [*assertive*](https://bitbucket.org/richierocks/assertive/) 260 | * Tony Fishettti's [*assertr*](https://github.com/tonyfischetti/assertr) 261 | * Hadley Wickham's [*assertthat*](https://github.com/hadley/assertthat) 262 | * Michel Lang's [*checkmate*](https://github.com/mllg/checkmate) 263 | * Joe Thorley's [*checkr*](https://github.com/poissonconsulting/checkr) 264 | * Joe Thorley's [*chk*](https://github.com/poissonconsulting/chk/) 265 | * Aviral Goel's [*contractr*](https://github.com/aviralg/contractr) 266 | * Stefan Bache's [*ensurer*](https://github.com/smbache/ensurer) 267 | * Brian Lee Yung Rowe's [*lambda.r*](https://github.com/zatonovo/lambda.r) 268 | * Kun Ren's [*rtype*](https://github.com/renkun-ken/rtype) 269 | * Duncan Temple Lang's [*TypeInfo*](https://bioconductor.org/packages/TypeInfo/) 270 | * Jim Hester's [*types*](https://github.com/jimhester/types) 271 | -------------------------------------------------------------------------------- /tests/testthat/test-native_types.R: -------------------------------------------------------------------------------- 1 | 2 | test_that("`Any` works", { 3 | expect_equal(Any()(1), 1) 4 | 5 | expect_equal(Any(1)(1), 1) 6 | expect_error(Any(2)(1)) 7 | }) 8 | 9 | test_that("`Logical` works", { 10 | expect_equal(Logical()(TRUE), TRUE) 11 | expect_error(Logical()(1)) 12 | 13 | expect_equal(Logical(1)(TRUE), TRUE) 14 | expect_error(Logical(2)(TRUE)) 15 | 16 | expect_equal(Logical(null_ok = TRUE)(NULL), NULL) 17 | expect_error(Logical(null_ok = FALSE)(NULL)) 18 | }) 19 | 20 | test_that("`Integer` works", { 21 | expect_equal(Integer()(1L), 1L) 22 | expect_error(Integer()(1)) 23 | 24 | expect_equal(Integer(1)(1L), 1L) 25 | expect_error(Integer(2)(1L)) 26 | 27 | expect_equal(Integer(null_ok = TRUE)(NULL), NULL) 28 | expect_error(Integer(null_ok = FALSE)(NULL)) 29 | }) 30 | 31 | test_that("`Double` works", { 32 | expect_equal(Double()(1), 1) 33 | expect_error(Double()(1L)) 34 | 35 | expect_equal(Double(1)(1), 1) 36 | expect_error(Double(2)(1)) 37 | 38 | expect_equal(Double(null_ok = TRUE)(NULL), NULL) 39 | expect_error(Double(null_ok = FALSE)(NULL)) 40 | }) 41 | 42 | test_that("`Character` works", { 43 | expect_equal(Character()("a"), "a") 44 | expect_error(Character()(1)) 45 | 46 | expect_equal(Character(1)("a"), "a") 47 | expect_error(Character(2)("a")) 48 | 49 | expect_equal(Character(null_ok = TRUE)(NULL), NULL) 50 | expect_error(Character(null_ok = FALSE)(NULL)) 51 | }) 52 | 53 | test_that("`Raw` works", { 54 | expect_equal(Raw()(as.raw("1")), as.raw("1")) 55 | expect_error(Raw()(1)) 56 | 57 | expect_equal(Raw(1)(as.raw("1")), as.raw("1")) 58 | expect_error(Raw(2)(as.raw("1"))) 59 | 60 | expect_equal(Raw(null_ok = TRUE)(NULL), NULL) 61 | expect_error(Raw(null_ok = FALSE)(NULL)) 62 | }) 63 | 64 | test_that("`List` works", { 65 | x <- list(a=1, b=2) 66 | y <- list(1, 2) 67 | df <- as.data.frame(x) 68 | expect_equal(List()(x), x) 69 | expect_error(List()(1)) 70 | 71 | expect_equal(List(2)(x), x) 72 | expect_error(List(1)(x)) 73 | 74 | expect_equal(List(each=Double())(x), x) 75 | expect_error(List(each=Integer())(x)) 76 | 77 | expect_equal(List(each=Double())(y), y) 78 | expect_error(List(each=Integer())(y)) 79 | 80 | expect_equal(List()(df), df) 81 | expect_error(List(data_frame_ok = FALSE)(df)) 82 | 83 | expect_equal(List(null_ok = TRUE)(NULL), NULL) 84 | expect_error(List(null_ok = FALSE)(NULL)) 85 | }) 86 | 87 | test_that("`Null` works", { 88 | expect_equal(Null()(NULL), NULL) 89 | expect_error(Null()(1)) 90 | }) 91 | 92 | test_that("`Closure` works", { 93 | expect_equal(Closure()(mean), mean) 94 | expect_error(Closure()(1)) 95 | 96 | expect_equal(Closure(null_ok = TRUE)(NULL), NULL) 97 | expect_error(Closure(null_ok = FALSE)(NULL)) 98 | }) 99 | 100 | test_that("`Special` works", { 101 | expect_equal(Special()(`<-`), `<-`) 102 | expect_error(Special()(1)) 103 | 104 | expect_equal(Special(null_ok = TRUE)(NULL), NULL) 105 | expect_error(Special(null_ok = FALSE)(NULL)) 106 | }) 107 | 108 | test_that("`Builtin` works", { 109 | expect_equal(Builtin()(max), max) 110 | expect_error(Builtin()(1)) 111 | 112 | expect_equal(Builtin(null_ok = TRUE)(NULL), NULL) 113 | expect_error(Builtin(null_ok = FALSE)(NULL)) 114 | }) 115 | 116 | test_that("`Environment` works", { 117 | expect_equal(Environment()(.GlobalEnv), .GlobalEnv) 118 | expect_error(Environment()(1)) 119 | 120 | expect_equal(Environment(null_ok = TRUE)(NULL), NULL) 121 | expect_error(Environment(null_ok = FALSE)(NULL)) 122 | }) 123 | 124 | test_that("`Symbol` works", { 125 | expect_equal(Symbol()(quote(a)), quote(a)) 126 | expect_error(Symbol()(1)) 127 | 128 | expect_equal(Symbol(null_ok = TRUE)(NULL), NULL) 129 | expect_error(Symbol(null_ok = FALSE)(NULL)) 130 | }) 131 | 132 | test_that("`Pairlist` works", { 133 | x <- pairlist(a=1, b=2) 134 | y <- pairlist(1, 2) 135 | expect_equal(Pairlist()(x), x) 136 | expect_error(Pairlist()(1)) 137 | 138 | expect_equal(Pairlist(2)(x), x) 139 | expect_error(Pairlist(1)(x)) 140 | 141 | expect_equal(Pairlist(each=Double())(x), x) 142 | expect_error(Pairlist(each=Integer())(x)) 143 | 144 | expect_equal(Pairlist(each=Double())(y), y) 145 | expect_error(Pairlist(each=Integer())(y)) 146 | 147 | expect_equal(Pairlist(null_ok = TRUE)(NULL), NULL) 148 | expect_error(Pairlist(null_ok = FALSE)(NULL)) 149 | }) 150 | 151 | test_that("`Language` works", { 152 | expect_equal(Language()(quote(a + b)), quote(a + b)) 153 | expect_error(Language()(1)) 154 | 155 | expect_equal(Language(null_ok = TRUE)(NULL), NULL) 156 | expect_error(Language(null_ok = FALSE)(NULL)) 157 | }) 158 | 159 | test_that("`Expression` works", { 160 | expect_equal(Expression()(expression(a)), expression(a)) 161 | expect_error(Expression()(1)) 162 | 163 | expect_equal(Expression(1)(expression(a)), expression(a)) 164 | expect_error(Expression(2)(expression(a))) 165 | 166 | expect_equal(Expression(null_ok = TRUE)(NULL), NULL) 167 | expect_error(Expression(null_ok = FALSE)(NULL)) 168 | }) 169 | 170 | test_that("`Function` works", { 171 | expect_equal(Function()(mean), mean) 172 | expect_error(Function()(1)) 173 | 174 | expect_equal(Function(null_ok = TRUE)(NULL), NULL) 175 | expect_error(Function(null_ok = FALSE)(NULL)) 176 | }) 177 | 178 | test_that("`Factor` works", { 179 | expect_equal(Factor()(factor("a")), factor("a")) 180 | expect_error(Factor()(1)) 181 | 182 | expect_equal(Factor(1)(factor("a")), factor("a")) 183 | expect_error(Factor(2)(factor("a"))) 184 | 185 | expect_equal(Factor(levels = "a")(factor("a")), factor("a")) 186 | expect_error(Factor(levels = "a")(factor("b"))) 187 | 188 | expect_equal(Factor(null_ok = TRUE)(NULL), NULL) 189 | expect_error(Factor(null_ok = FALSE)(NULL)) 190 | }) 191 | 192 | test_that("`Matrix` works", { 193 | expect_equal(Matrix()(matrix(1)), matrix(1)) 194 | expect_error(Matrix()(1)) 195 | 196 | expect_equal(Matrix(1)(matrix(1)), matrix(1)) 197 | expect_error(Matrix(2)(matrix(1))) 198 | 199 | expect_equal(Matrix(,1)(matrix(1)), matrix(1)) 200 | expect_error(Matrix(,2)(matrix(1))) 201 | 202 | expect_equal(Matrix(null_ok = TRUE)(NULL), NULL) 203 | expect_error(Matrix(null_ok = FALSE)(NULL)) 204 | }) 205 | 206 | test_that("`Array` works", { 207 | expect_equal(Array()(matrix(1)), matrix(1)) 208 | expect_error(Array()(1)) 209 | 210 | expect_equal(Array(c(1,1))(matrix(1)), matrix(1)) 211 | expect_error(Array(c(1,2))(matrix(1))) 212 | 213 | expect_equal(Array(null_ok = TRUE)(NULL), NULL) 214 | expect_error(Array(null_ok = FALSE)(NULL)) 215 | }) 216 | 217 | test_that("`Data.frame` works", { 218 | expect_equal(Data.frame()(data.frame(a=1, b=2L)), data.frame(a=1, b=2L)) 219 | expect_error(Data.frame()(1)) 220 | 221 | expect_equal(Data.frame(1)(data.frame(a=1, b=2L)), data.frame(a=1, b=2L)) 222 | expect_error(Data.frame(2)(data.frame(a=1, b=2L))) 223 | 224 | expect_equal(Data.frame(,2)(data.frame(a=1, b=2L)), data.frame(a=1, b=2L)) 225 | expect_error(Data.frame(,1)(data.frame(a=1, b=2L))) 226 | 227 | expect_equal(Data.frame(each = Double())(data.frame(a=1, b=2)), data.frame(a=1, b=2)) 228 | expect_error(Data.frame(each = Double())(data.frame(a=1, b=2L))) 229 | 230 | expect_equal(Data.frame(null_ok = TRUE)(NULL), NULL) 231 | expect_error(Data.frame(null_ok = FALSE)(NULL)) 232 | }) 233 | 234 | test_that("`Date` works", { 235 | x <- Sys.Date() 236 | expect_equal(Date()(x), x) 237 | expect_error(Date()(1)) 238 | 239 | expect_equal(Date(1)(x), x) 240 | expect_error(Date(2)(x)) 241 | 242 | expect_equal(Date(null_ok = TRUE)(NULL), NULL) 243 | expect_error(Date(null_ok = FALSE)(NULL)) 244 | }) 245 | 246 | test_that("`Time` works", { 247 | x <- Sys.time() 248 | expect_equal(Time()(x), x) 249 | expect_error(Time()(1)) 250 | 251 | expect_equal(Time(1)(x), x) 252 | expect_error(Time(2)(x)) 253 | 254 | expect_equal(Time(null_ok = TRUE)(NULL), NULL) 255 | expect_error(Time(null_ok = FALSE)(NULL)) 256 | }) 257 | 258 | test_that("`Dots` works", { 259 | x <- list(a=1, b=2) 260 | y <- list(1, 2) 261 | 262 | expect_equal(Dots(2)(x), x) 263 | expect_error(Dots(1)(x)) 264 | 265 | expect_equal(Dots(each=Double())(x), x) 266 | expect_error(Dots(each=Integer())(x)) 267 | 268 | expect_equal(Dots(each=Double())(y), y) 269 | expect_error(Dots(each=Integer())(y)) 270 | }) 271 | -------------------------------------------------------------------------------- /.github/workflows/revdep.yaml: -------------------------------------------------------------------------------- 1 | # This workflow creates many jobs, run only when a branch is created 2 | on: 3 | push: 4 | branches: 5 | - "revdep*" # never run automatically on main branch 6 | 7 | name: revdep 8 | 9 | jobs: 10 | matrix: 11 | runs-on: ubuntu-22.04 12 | outputs: 13 | matrix: ${{ steps.set-matrix.outputs.matrix }} 14 | 15 | name: Collect revdeps 16 | 17 | env: 18 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 19 | RSPM: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest 20 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 21 | # prevent rgl issues because no X11 display is available 22 | RGL_USE_NULL: true 23 | # Begin custom: env vars 24 | # End custom: env vars 25 | 26 | steps: 27 | - name: Check rate limits 28 | run: | 29 | curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit 30 | shell: bash 31 | 32 | - uses: actions/checkout@v4 33 | 34 | # FIXME: Avoid reissuing succesful jobs 35 | # https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#list-jobs-for-a-workflow-run 36 | # https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#workflow-runs 37 | - id: set-matrix 38 | run: | 39 | package <- read.dcf("DESCRIPTION")[, "Package"][[1]] 40 | deps <- tools:::package_dependencies(package, reverse = TRUE, which = c("Depends", "Imports", "LinkingTo", "Suggests"))[[1]] 41 | json <- paste0( 42 | '{"package":[', 43 | paste0('"', deps, '"', collapse = ","), 44 | ']}' 45 | ) 46 | writeLines(json) 47 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 48 | shell: Rscript {0} 49 | 50 | check-matrix: 51 | runs-on: ubuntu-22.04 52 | needs: matrix 53 | steps: 54 | - name: Install json2yaml 55 | run: | 56 | sudo npm install -g json2yaml 57 | 58 | - name: Check matrix definition 59 | run: | 60 | matrix='${{ needs.matrix.outputs.matrix }}' 61 | echo $matrix 62 | echo $matrix | jq . 63 | echo $matrix | json2yaml 64 | 65 | R-CMD-check: 66 | needs: matrix 67 | 68 | runs-on: ubuntu-22.04 69 | 70 | name: 'revdep: ${{ matrix.package }}' 71 | 72 | # Begin custom: services 73 | # End custom: services 74 | 75 | strategy: 76 | fail-fast: false 77 | matrix: ${{fromJson(needs.matrix.outputs.matrix)}} 78 | 79 | env: 80 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 81 | RSPM: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest 82 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 83 | # prevent rgl issues because no X11 display is available 84 | RGL_USE_NULL: true 85 | # Begin custom: env vars 86 | # End custom: env vars 87 | 88 | steps: 89 | - name: Check rate limits 90 | run: | 91 | curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit 92 | shell: bash 93 | 94 | - uses: actions/checkout@v4 95 | 96 | # Begin custom: before install 97 | # End custom: before install 98 | 99 | - name: Use RSPM 100 | run: | 101 | mkdir -p /home/runner/work/_temp/Library 102 | echo 'local({release <- system2("lsb_release", "-sc", stdout = TRUE); options(repos=c(CRAN = paste0("https://packagemanager.rstudio.com/all/__linux__/", release, "/latest")), HTTPUserAgent = sprintf("R/%s R (%s)", getRversion(), paste(getRversion(), R.version$platform, R.version$arch, R.version$os)))}); .libPaths("/home/runner/work/_temp/Library")' | sudo tee /etc/R/Rprofile.site 103 | 104 | - name: Install remotes 105 | run: | 106 | if (!requireNamespace("curl", quietly = TRUE)) install.packages("curl") 107 | if (!requireNamespace("remotes", quietly = TRUE)) install.packages("remotes") 108 | shell: Rscript {0} 109 | 110 | - uses: r-lib/actions/setup-pandoc@v2 111 | 112 | - name: Install system dependencies 113 | if: runner.os == 'Linux' 114 | run: | 115 | sudo apt-get update -y 116 | Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "22.04")); package <- "${{ matrix.package }}"; deps <- tools::package_dependencies(package, which = "Suggests")[[1]]; lapply(c(package, deps), function(x) { writeLines(remotes::system_requirements("ubuntu", "22.04", package = x)) })' | sort | uniq > .github/deps.sh 117 | cat .github/deps.sh 118 | sudo sh < .github/deps.sh 119 | 120 | - name: Install package 121 | run: | 122 | package <- "${{ matrix.package }}" 123 | install.packages(package, dependencies = TRUE) 124 | remotes::install_cran("rcmdcheck") 125 | shell: Rscript {0} 126 | 127 | - name: Session info old 128 | run: | 129 | options(width = 100) 130 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") 131 | pkgs <- installed.packages()[, "Package"] 132 | sessioninfo::session_info(pkgs, include_base = TRUE) 133 | shell: Rscript {0} 134 | 135 | # Begin custom: after install 136 | # End custom: after install 137 | 138 | - name: Check old 139 | env: 140 | _R_CHECK_CRAN_INCOMING_: false 141 | _R_CHECK_SYSTEM_CLOCK_: false 142 | _R_CHECK_FUTURE_FILE_TIMESTAMPS_: false 143 | # Avoid downloading binary package from RSPM 144 | run: | 145 | package <- "${{ matrix.package }}" 146 | options(HTTPUserAgent = "gha") 147 | path <- download.packages(package, destdir = ".github")[, 2] 148 | print(path) 149 | 150 | dir <- file.path("revdep", package) 151 | dir.create(dir, showWarnings = FALSE, recursive = TRUE) 152 | check <- rcmdcheck::rcmdcheck(path, args = c("--no-manual", "--as-cran"), error_on = "never", check_dir = file.path(dir, "check")) 153 | file.rename(file.path(dir, "check"), file.path(dir, "old")) 154 | saveRDS(check, file.path(dir, "old.rds")) 155 | shell: Rscript {0} 156 | 157 | - name: Install local package 158 | run: | 159 | remotes::install_local(".", force = TRUE) 160 | shell: Rscript {0} 161 | 162 | - name: Session info new 163 | run: | 164 | options(width = 100) 165 | pkgs <- installed.packages()[, "Package"] 166 | sessioninfo::session_info(pkgs, include_base = TRUE) 167 | shell: Rscript {0} 168 | 169 | - name: Check new 170 | env: 171 | _R_CHECK_CRAN_INCOMING_: false 172 | _R_CHECK_SYSTEM_CLOCK_: false 173 | _R_CHECK_FUTURE_FILE_TIMESTAMPS_: false 174 | run: | 175 | package <- "${{ matrix.package }}" 176 | path <- dir(".github", pattern = paste0("^", package), full.names = TRUE)[[1]] 177 | print(path) 178 | 179 | dir <- file.path("revdep", package) 180 | check <- rcmdcheck::rcmdcheck(path, args = c("--no-manual", "--as-cran"), error_on = "never", check_dir = file.path(dir, "check")) 181 | file.rename(file.path(dir, "check"), file.path(dir, "new")) 182 | saveRDS(check, file.path(dir, "new.rds")) 183 | shell: Rscript {0} 184 | 185 | - name: Compare 186 | run: | 187 | package <- "${{ matrix.package }}" 188 | dir <- file.path("revdep", package) 189 | old <- readRDS(file.path(dir, "old.rds")) 190 | new <- readRDS(file.path(dir, "new.rds")) 191 | compare <- rcmdcheck::compare_checks(old, new) 192 | compare 193 | cmp <- compare$cmp 194 | if (!identical(cmp[cmp$which == "old", "output"], cmp[cmp$which == "new", "output"])) { 195 | if (!requireNamespace("waldo", quietly = TRUE)) install.packages("waldo") 196 | print(waldo::compare(old, new)) 197 | 198 | stop("Check output differs.") 199 | } 200 | shell: Rscript {0} 201 | 202 | - name: Upload check results 203 | if: failure() 204 | uses: actions/upload-artifact@main 205 | with: 206 | name: ${{ matrix.package }}-results 207 | path: revdep/${{ matrix.package }} 208 | 209 | - name: Check rate limits 210 | if: always() 211 | run: | 212 | curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit 213 | shell: bash 214 | -------------------------------------------------------------------------------- /docs/pkgdown.css: -------------------------------------------------------------------------------- 1 | /* Sticky footer */ 2 | 3 | /** 4 | * Basic idea: https://philipwalton.github.io/solved-by-flexbox/demos/sticky-footer/ 5 | * Details: https://github.com/philipwalton/solved-by-flexbox/blob/master/assets/css/components/site.css 6 | * 7 | * .Site -> body > .container 8 | * .Site-content -> body > .container .row 9 | * .footer -> footer 10 | * 11 | * Key idea seems to be to ensure that .container and __all its parents__ 12 | * have height set to 100% 13 | * 14 | */ 15 | 16 | html, body { 17 | height: 100%; 18 | } 19 | 20 | body { 21 | position: relative; 22 | } 23 | 24 | body > .container { 25 | display: flex; 26 | height: 100%; 27 | flex-direction: column; 28 | } 29 | 30 | body > .container .row { 31 | flex: 1 0 auto; 32 | } 33 | 34 | footer { 35 | margin-top: 45px; 36 | padding: 35px 0 36px; 37 | border-top: 1px solid #e5e5e5; 38 | color: #666; 39 | display: flex; 40 | flex-shrink: 0; 41 | } 42 | footer p { 43 | margin-bottom: 0; 44 | } 45 | footer div { 46 | flex: 1; 47 | } 48 | footer .pkgdown { 49 | text-align: right; 50 | } 51 | footer p { 52 | margin-bottom: 0; 53 | } 54 | 55 | img.icon { 56 | float: right; 57 | } 58 | 59 | /* Ensure in-page images don't run outside their container */ 60 | .contents img { 61 | max-width: 100%; 62 | height: auto; 63 | } 64 | 65 | /* Fix bug in bootstrap (only seen in firefox) */ 66 | summary { 67 | display: list-item; 68 | } 69 | 70 | /* Typographic tweaking ---------------------------------*/ 71 | 72 | .contents .page-header { 73 | margin-top: calc(-60px + 1em); 74 | } 75 | 76 | dd { 77 | margin-left: 3em; 78 | } 79 | 80 | /* Section anchors ---------------------------------*/ 81 | 82 | a.anchor { 83 | display: none; 84 | margin-left: 5px; 85 | width: 20px; 86 | height: 20px; 87 | 88 | background-image: url(./link.svg); 89 | background-repeat: no-repeat; 90 | background-size: 20px 20px; 91 | background-position: center center; 92 | } 93 | 94 | h1:hover .anchor, 95 | h2:hover .anchor, 96 | h3:hover .anchor, 97 | h4:hover .anchor, 98 | h5:hover .anchor, 99 | h6:hover .anchor { 100 | display: inline-block; 101 | } 102 | 103 | /* Fixes for fixed navbar --------------------------*/ 104 | 105 | .contents h1, .contents h2, .contents h3, .contents h4 { 106 | padding-top: 60px; 107 | margin-top: -40px; 108 | } 109 | 110 | /* Navbar submenu --------------------------*/ 111 | 112 | .dropdown-submenu { 113 | position: relative; 114 | } 115 | 116 | .dropdown-submenu>.dropdown-menu { 117 | top: 0; 118 | left: 100%; 119 | margin-top: -6px; 120 | margin-left: -1px; 121 | border-radius: 0 6px 6px 6px; 122 | } 123 | 124 | .dropdown-submenu:hover>.dropdown-menu { 125 | display: block; 126 | } 127 | 128 | .dropdown-submenu>a:after { 129 | display: block; 130 | content: " "; 131 | float: right; 132 | width: 0; 133 | height: 0; 134 | border-color: transparent; 135 | border-style: solid; 136 | border-width: 5px 0 5px 5px; 137 | border-left-color: #cccccc; 138 | margin-top: 5px; 139 | margin-right: -10px; 140 | } 141 | 142 | .dropdown-submenu:hover>a:after { 143 | border-left-color: #ffffff; 144 | } 145 | 146 | .dropdown-submenu.pull-left { 147 | float: none; 148 | } 149 | 150 | .dropdown-submenu.pull-left>.dropdown-menu { 151 | left: -100%; 152 | margin-left: 10px; 153 | border-radius: 6px 0 6px 6px; 154 | } 155 | 156 | /* Sidebar --------------------------*/ 157 | 158 | #pkgdown-sidebar { 159 | margin-top: 30px; 160 | position: -webkit-sticky; 161 | position: sticky; 162 | top: 70px; 163 | } 164 | 165 | #pkgdown-sidebar h2 { 166 | font-size: 1.5em; 167 | margin-top: 1em; 168 | } 169 | 170 | #pkgdown-sidebar h2:first-child { 171 | margin-top: 0; 172 | } 173 | 174 | #pkgdown-sidebar .list-unstyled li { 175 | margin-bottom: 0.5em; 176 | } 177 | 178 | /* bootstrap-toc tweaks ------------------------------------------------------*/ 179 | 180 | /* All levels of nav */ 181 | 182 | nav[data-toggle='toc'] .nav > li > a { 183 | padding: 4px 20px 4px 6px; 184 | font-size: 1.5rem; 185 | font-weight: 400; 186 | color: inherit; 187 | } 188 | 189 | nav[data-toggle='toc'] .nav > li > a:hover, 190 | nav[data-toggle='toc'] .nav > li > a:focus { 191 | padding-left: 5px; 192 | color: inherit; 193 | border-left: 1px solid #878787; 194 | } 195 | 196 | nav[data-toggle='toc'] .nav > .active > a, 197 | nav[data-toggle='toc'] .nav > .active:hover > a, 198 | nav[data-toggle='toc'] .nav > .active:focus > a { 199 | padding-left: 5px; 200 | font-size: 1.5rem; 201 | font-weight: 400; 202 | color: inherit; 203 | border-left: 2px solid #878787; 204 | } 205 | 206 | /* Nav: second level (shown on .active) */ 207 | 208 | nav[data-toggle='toc'] .nav .nav { 209 | display: none; /* Hide by default, but at >768px, show it */ 210 | padding-bottom: 10px; 211 | } 212 | 213 | nav[data-toggle='toc'] .nav .nav > li > a { 214 | padding-left: 16px; 215 | font-size: 1.35rem; 216 | } 217 | 218 | nav[data-toggle='toc'] .nav .nav > li > a:hover, 219 | nav[data-toggle='toc'] .nav .nav > li > a:focus { 220 | padding-left: 15px; 221 | } 222 | 223 | nav[data-toggle='toc'] .nav .nav > .active > a, 224 | nav[data-toggle='toc'] .nav .nav > .active:hover > a, 225 | nav[data-toggle='toc'] .nav .nav > .active:focus > a { 226 | padding-left: 15px; 227 | font-weight: 500; 228 | font-size: 1.35rem; 229 | } 230 | 231 | /* orcid ------------------------------------------------------------------- */ 232 | 233 | .orcid { 234 | font-size: 16px; 235 | color: #A6CE39; 236 | /* margins are required by official ORCID trademark and display guidelines */ 237 | margin-left:4px; 238 | margin-right:4px; 239 | vertical-align: middle; 240 | } 241 | 242 | /* Reference index & topics ----------------------------------------------- */ 243 | 244 | .ref-index th {font-weight: normal;} 245 | 246 | .ref-index td {vertical-align: top; min-width: 100px} 247 | .ref-index .icon {width: 40px;} 248 | .ref-index .alias {width: 40%;} 249 | .ref-index-icons .alias {width: calc(40% - 40px);} 250 | .ref-index .title {width: 60%;} 251 | 252 | .ref-arguments th {text-align: right; padding-right: 10px;} 253 | .ref-arguments th, .ref-arguments td {vertical-align: top; min-width: 100px} 254 | .ref-arguments .name {width: 20%;} 255 | .ref-arguments .desc {width: 80%;} 256 | 257 | /* Nice scrolling for wide elements --------------------------------------- */ 258 | 259 | table { 260 | display: block; 261 | overflow: auto; 262 | } 263 | 264 | /* Syntax highlighting ---------------------------------------------------- */ 265 | 266 | pre, code, pre code { 267 | background-color: #f8f8f8; 268 | color: #333; 269 | } 270 | pre, pre code { 271 | white-space: pre-wrap; 272 | word-break: break-all; 273 | overflow-wrap: break-word; 274 | } 275 | 276 | pre { 277 | border: 1px solid #eee; 278 | } 279 | 280 | pre .img, pre .r-plt { 281 | margin: 5px 0; 282 | } 283 | 284 | pre .img img, pre .r-plt img { 285 | background-color: #fff; 286 | } 287 | 288 | code a, pre a { 289 | color: #375f84; 290 | } 291 | 292 | a.sourceLine:hover { 293 | text-decoration: none; 294 | } 295 | 296 | .fl {color: #1514b5;} 297 | .fu {color: #000000;} /* function */ 298 | .ch,.st {color: #036a07;} /* string */ 299 | .kw {color: #264D66;} /* keyword */ 300 | .co {color: #888888;} /* comment */ 301 | 302 | .error {font-weight: bolder;} 303 | .warning {font-weight: bolder;} 304 | 305 | /* Clipboard --------------------------*/ 306 | 307 | .hasCopyButton { 308 | position: relative; 309 | } 310 | 311 | .btn-copy-ex { 312 | position: absolute; 313 | right: 0; 314 | top: 0; 315 | visibility: hidden; 316 | } 317 | 318 | .hasCopyButton:hover button.btn-copy-ex { 319 | visibility: visible; 320 | } 321 | 322 | /* headroom.js ------------------------ */ 323 | 324 | .headroom { 325 | will-change: transform; 326 | transition: transform 200ms linear; 327 | } 328 | .headroom--pinned { 329 | transform: translateY(0%); 330 | } 331 | .headroom--unpinned { 332 | transform: translateY(-100%); 333 | } 334 | 335 | /* mark.js ----------------------------*/ 336 | 337 | mark { 338 | background-color: rgba(255, 255, 51, 0.5); 339 | border-bottom: 2px solid rgba(255, 153, 51, 0.3); 340 | padding: 1px; 341 | } 342 | 343 | /* vertical spacing after htmlwidgets */ 344 | .html-widget { 345 | margin-bottom: 10px; 346 | } 347 | 348 | /* fontawesome ------------------------ */ 349 | 350 | .fab { 351 | font-family: "Font Awesome 5 Brands" !important; 352 | } 353 | 354 | /* don't display links in code chunks when printing */ 355 | /* source: https://stackoverflow.com/a/10781533 */ 356 | @media print { 357 | code a:link:after, code a:visited:after { 358 | content: ""; 359 | } 360 | } 361 | 362 | /* Section anchors --------------------------------- 363 | Added in pandoc 2.11: https://github.com/jgm/pandoc-templates/commit/9904bf71 364 | */ 365 | 366 | div.csl-bib-body { } 367 | div.csl-entry { 368 | clear: both; 369 | } 370 | .hanging-indent div.csl-entry { 371 | margin-left:2em; 372 | text-indent:-2em; 373 | } 374 | div.csl-left-margin { 375 | min-width:2em; 376 | float:left; 377 | } 378 | div.csl-right-inline { 379 | margin-left:2em; 380 | padding-left:1em; 381 | } 382 | div.csl-indent { 383 | margin-left: 2em; 384 | } 385 | -------------------------------------------------------------------------------- /docs/reference/index.html: -------------------------------------------------------------------------------- 1 | 2 | Function reference • typed 6 | Skip to contents 7 | 8 | 9 |
    44 |
    45 |
    48 | 49 |
    50 |

    All functions

    51 | 52 | 53 | 54 | 55 |
    56 | 57 | 58 | 59 | 60 |
    61 | 62 | as_assertion_factory() 63 |
    64 |
    Build a new type
    65 |
    66 | 67 | Any() Logical() Integer() Double() Character() Raw() List() Null() Closure() Special() Builtin() Environment() Symbol() Pairlist() Language() Expression() Function() Factor() Matrix() Array() Data.frame() Date() Time() Dots() 68 |
    69 |
    Assertion factories of package 'typed'
    70 |
    71 | 72 | check_output() check_arg() 73 |
    74 |
    Check Argument Types and Return Type
    75 |
    76 | 77 | `?`() declare() 78 |
    79 |
    Set Variable Types, Argument Types and Return Types.
    80 |
    81 | 82 | process_assertion_factory_dots() 83 |
    84 |
    Process assertion factory dots
    85 |
    86 | 87 | use_typed() 88 |
    89 |
    Use the 'typed' package
    90 |
    91 |
    92 | 93 | 94 |
    97 | 98 | 101 | 102 |
    103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /docs/reference/static_typing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Static typing — ? • typed 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
    72 |
    73 | 112 | 113 | 114 | 115 |
    116 | 117 |
    118 |
    119 | 124 | 125 |
    126 |

    Use ? to set a function's return type, argument types, or variable types 127 | in the body of the function.

    128 |
    129 | 130 |
    `?`(lhs, rhs)
    131 | 
    132 | check_output(.output, .assertion, ...)
    133 | 
    134 | check_arg(.arg, .assertion, ..., .bind = FALSE)
    135 | 
    136 | declare(x, assertion, value, const = FALSE)
    137 | 138 |

    Arguments

    139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 |
    lhs

    lhs

    rhs

    rhs

    .output

    function output

    .assertion

    an assertion

    ...

    additional arguments passed to assertion

    .arg

    function argument

    .bind

    whether to actively bind the argument so it cannot be modified 168 | unless it satisfies the assertion

    x

    variable name as a string

    assertion

    a function

    value

    an optional value

    const

    whether to declare x as a constant

    187 | 188 | 189 |

    Examples

    190 |
    numeric ? function (x= ?numeric) { 191 | numeric ? y <- 2 192 | res <- x + y 193 | res 194 | }
    #> # typed function 195 | #> function (x) 196 | #> { 197 | #> check_arg(x, numeric) 198 | #> declare("y", numeric, value = 2) 199 | #> res <- x + y 200 | #> check_output(res, numeric) 201 | #> } 202 | #> <environment: 0x000000002b89e240> 203 | #> # Return type: numeric 204 | #> # Arg types: 205 | #> # x: numeric
    206 |
    207 | 212 |
    213 | 214 | 215 |
    216 | 219 | 220 |
    221 |

    Site built with pkgdown 1.5.1.

    222 |
    223 | 224 |
    225 |
    226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [![Travis build 5 | status](https://travis-ci.com/moodymudskipper/typed.svg?branch=iteration2)](https://travis-ci.com/moodymudskipper/typed) 6 | [![Codecov test 7 | coverage](https://codecov.io/gh/moodymudskipper/typed/branch/master/graph/badge.svg)](https://codecov.io/gh/moodymudskipper/typed?branch=master) 8 | 9 | 10 | # typed 11 | 12 | *{typed}* implements a type system for R, it has 3 main features: 13 | 14 | - set variable types in a script or the body of a function, so they 15 | can’t be assigned illegal values 16 | - set argument types in a function definition 17 | - set return type of a function 18 | 19 | The user can define their own types, or leverage assertions from other 20 | packages. 21 | 22 | Under the hood variable types use active bindings, so once a variable is 23 | restricted by an assertion, it cannot be modified in a way that would 24 | not satisfy it. 25 | 26 | ## Installation 27 | 28 | Install CRAN version with: 29 | 30 | ``` r 31 | install.packages("typed") 32 | ``` 33 | 34 | or development version with : 35 | 36 | ``` r 37 | remotes::install_github("moodymudskipper/typed") 38 | ``` 39 | 40 | And attach with : 41 | 42 | ``` r 43 | # masking warning about overriding `?` 44 | library(typed, warn.conflicts = FALSE) 45 | ``` 46 | 47 | ## Set variable type 48 | 49 | ### Question mark notation and `declare` 50 | 51 | Here are examples on how we would set types 52 | 53 | ``` r 54 | Character() ? x # restrict x to "character" type 55 | x <- "a" 56 | x 57 | #> [1] "a" 58 | 59 | Integer(3) ? y <- 1:3 # restrict y to "integer" type of length 3 60 | y 61 | #> [1] 1 2 3 62 | ``` 63 | 64 | We cannot assign values of the wrong type to `x` and `y` anymore. 65 | 66 | ``` r 67 | x <- 2 68 | #> Error: type mismatch 69 | #> `typeof(value)`: "double" 70 | #> `expected`: "character" 71 | 72 | y <- 4:5 73 | #> Error: length mismatch 74 | #> `length(value)`: 2 75 | #> `expected`: 3 76 | ``` 77 | 78 | But the right type will work. 79 | 80 | ``` r 81 | x <- c("b", "c") 82 | 83 | y <- c(1L, 10L, 100L) 84 | ``` 85 | 86 | `declare` is a strict equivalent, slightly more efficient, which looks 87 | like `base::assign`. 88 | 89 | ``` r 90 | declare("x", Character()) 91 | x <- "a" 92 | x 93 | #> [1] "a" 94 | 95 | declare("y", Integer(3), 1:3) 96 | y 97 | #> [1] 1 2 3 98 | ``` 99 | 100 | ### Assertion factories and assertions 101 | 102 | `Integer` and `Character` are function factories (functions that return 103 | functions), thus `Integer(3)` and `Character()` are functions. 104 | 105 | The latter functions operate checks on a value and in case of success 106 | return this value, generally unmodified. For instance : 107 | 108 | ``` r 109 | Integer(3)(1:2) 110 | #> Error: length mismatch 111 | #> `length(value)`: 2 112 | #> `expected`: 3 113 | 114 | Character()(3) 115 | #> Error: type mismatch 116 | #> `typeof(value)`: "double" 117 | #> `expected`: "character" 118 | ``` 119 | 120 | We call `Integer(3)` and `Character()` assertions, and we call `Integer` 121 | and `Character` assertion factories (or just types, with then we must be 122 | careful not to confuse them with atomic types returned by the `typeof` 123 | function). 124 | 125 | The package contains many assertion factories (see 126 | `?assertion_factories`), the main ones are: 127 | 128 | - `Any` (No default restriction) 129 | - `Logical` 130 | - `Integer` 131 | - `Double` 132 | - `Character` 133 | - `List` 134 | - `Environment` 135 | - `Factor` 136 | - `Matrix` 137 | - `Data.frame` 138 | - `Date` 139 | - `Time` (POSIXct) 140 | 141 | ### Advanced type restriction using arguments 142 | 143 | As we’ve seen with `Integer(3)`, passing arguments to a assertion 144 | factory restricts the type. 145 | 146 | For instance `Integer` has arguments `length` `null_ok` and `...`. We 147 | already used `length`, `null_ok` is convenient to allow a default `NULL` 148 | value in addition to the `"integer"` type. 149 | 150 | The arguments can differ between assertion factories, for instance 151 | `Data.frame` has `nrow`, `ncol`, `each`, `null_ok` and `...` 152 | 153 | ``` r 154 | Data.frame() ? x <- iris 155 | Data.frame(ncol = 2) ? x <- iris 156 | #> Error: Column number mismatch 157 | #> `ncol(value)`: 5 158 | #> `expected`: 2 159 | Data.frame(each = Double()) ? x <- iris 160 | #> Error: column 5 ("Species") type mismatch 161 | #> `typeof(value)`: "integer" 162 | #> `expected`: "double" 163 | ``` 164 | 165 | In the dots we can use arguments named as functions and with the value 166 | of the expected result. 167 | 168 | ``` r 169 | # Integer has no anyNA arg but we can still use it because a function named 170 | # this way exists 171 | Integer(anyNA = FALSE) ? x <- c(1L, 2L, NA) 172 | #> Error: `anyNA` mismatch 173 | #> `anyNA(value)`: TRUE 174 | #> `expected`: FALSE 175 | ``` 176 | 177 | Useful arguments might be for instance, `anyDuplicated = 0L`, 178 | `names = NULL`, `attributes = NULL`… Any available function can be used. 179 | 180 | That makes assertion factories very flexible! If it is still not 181 | flexible enough, we can provide arguments arguments named `...` to 182 | functional factories to add a custom restriction, this is usually better 183 | done by defining a wrapper. 184 | 185 | ``` r 186 | Character(1, ... = "`value` is not a fruit!" ~ . %in% c("apple", "pear", "cherry")) ? 187 | x <- "potatoe" 188 | #> Error: `value` is not a fruit! 189 | #> `value %in% c("apple", "pear", "cherry")`: FALSE 190 | #> `expected`: TRUE 191 | ``` 192 | 193 | This is often better done by defining a wrapper as shown below. 194 | 195 | ### Constants 196 | 197 | To define a constant, we just surround the variable by parentheses 198 | (think of them as a protection) 199 | 200 | ``` r 201 | Double() ? (x) <- 1 202 | x <- 2 203 | #> Error: Can't assign to a constant 204 | 205 | # defining a type is optional 206 | ? (y) <- 1 207 | y <- 2 208 | #> Error: Can't assign to a constant 209 | ``` 210 | 211 | ### Set a function’s argument type 212 | 213 | We can set argument types this way : 214 | 215 | ``` r 216 | add <- ? function (x= ? Double(), y= 1 ? Double()) { 217 | x + y 218 | } 219 | ``` 220 | 221 | Note that we started the definition with a `?`, and that we gave a 222 | default to `y`, but not `x`. Note also the `=` sign next to `x`, 223 | necessary even when we have no default value. If you forget it you’ll 224 | have an error “unexpected `?` in …”. 225 | 226 | This created the following function, by adding checks at the top of the 227 | body 228 | 229 | ``` r 230 | add 231 | #> # typed function 232 | #> function (x, y = 1) 233 | #> { 234 | #> check_arg(x, Double()) 235 | #> check_arg(y, Double()) 236 | #> x + y 237 | #> } 238 | #> # Arg types: 239 | #> # x: Double() 240 | #> # y: Double() 241 | ``` 242 | 243 | Let’s test it by providing a right and wrong type. 244 | 245 | ``` r 246 | add(2, 3) 247 | #> [1] 5 248 | add(2, 3L) 249 | #> Error: In `add(2, 3L)` at `check_arg(y, Double())`: 250 | #> wrong argument to function, type mismatch 251 | #> `typeof(value)`: "integer" 252 | #> `expected`: "double" 253 | ``` 254 | 255 | If we want to restrict `x` and `y` to the type “integer” in the rest of 256 | the body, so they cannot be overwritten by character for instance,we can 257 | use the `?+` notation : 258 | 259 | ``` r 260 | add <- ? function (x= ?+ Double(), y= 1 ?+ Double()) { 261 | x + y 262 | } 263 | 264 | add 265 | #> # typed function 266 | #> function (x, y = 1) 267 | #> { 268 | #> check_arg(x, Double(), .bind = TRUE) 269 | #> check_arg(y, Double(), .bind = TRUE) 270 | #> x + y 271 | #> } 272 | #> # Arg types: 273 | #> # x: Double() 274 | #> # y: Double() 275 | ``` 276 | 277 | We see that it is translated into a `check_arg` call containing a 278 | `.bind = TRUE` argument. 279 | 280 | ## Set a function’s return type 281 | 282 | To set a return type we use `?` before the function definition as in the 283 | previous section, but we type an assertion on the left hand side. 284 | 285 | ``` r 286 | add_or_subtract <- Double() ? function (x, y, subtract = FALSE) { 287 | if(subtract) return(x - y) 288 | x + y 289 | } 290 | add_or_subtract 291 | #> # typed function 292 | #> function (x, y, subtract = FALSE) 293 | #> { 294 | #> if (subtract) 295 | #> return(check_output(x - y, Double())) 296 | #> check_output(x + y, Double()) 297 | #> } 298 | #> # Return type: Double() 299 | ``` 300 | 301 | We see that the returned values have been wrapped inside `check_output` 302 | calls. 303 | 304 | # Use type in a package and define your own types 305 | 306 | See `vignette("typed-in-packaged", "typed")` or the Article section if 307 | you’re browsing the pkgdown website. 308 | 309 | ## Acknowledgements 310 | 311 | This is inspired in good part by Jim Hester and Gabor Csardi’s work and 312 | many great efforts on static typing, assertions, or annotations in R, in 313 | particular: 314 | 315 | - Gabor Csardy’s [*argufy*](https://github.com/gaborcsardi/argufy) 316 | - Richie Cotton’s 317 | [*assertive*](https://bitbucket.org/richierocks/assertive/) 318 | - Tony Fishettti’s [*assertr*](https://github.com/tonyfischetti/assertr) 319 | - Hadley Wickham’s [*assertthat*](https://github.com/hadley/assertthat) 320 | - Michel Lang’s [*checkmate*](https://github.com/mllg/checkmate) 321 | - Joe Thorley’s [*checkr*](https://github.com/poissonconsulting/checkr) 322 | - Joe Thorley’s [*chk*](https://github.com/poissonconsulting/chk/) 323 | - Aviral Goel’s [*contractr*](https://github.com/aviralg/contractr) 324 | - Stefan Bache’s [*ensurer*](https://github.com/smbache/ensurer) 325 | - Brian Lee Yung Rowe’s 326 | [*lambda.r*](https://github.com/zatonovo/lambda.r) 327 | - Kun Ren’s [*rtype*](https://github.com/renkun-ken/rtype) 328 | - Duncan Temple Lang’s 329 | [*TypeInfo*](https://bioconductor.org/packages/TypeInfo/) 330 | - Jim Hester’s [*types*](https://github.com/jimhester/types) 331 | --------------------------------------------------------------------------------