Title: | Pattern Matching and Enumerated Types in R |
---|---|
Description: | Inspired by pattern matching and enum types in Rust and many functional programming languages, this package offers an updated version of the 'switch' function called 'Match' that accepts atomic values, functions, expressions, and enum variants. Conditions and return expressions are separated by '->' and multiple conditions can be associated with the same return expression using '|'. 'Match' also includes support for 'fallthrough'. The package also replicates the Result and Option enums from Rust. |
Authors: | Christopher Mann <[email protected]> |
Maintainer: | Christopher Mann <[email protected]> |
License: | MIT + file LICENSE |
Version: | 0.1.0 |
Built: | 2024-11-25 05:39:15 UTC |
Source: | https://github.com/cran/matchr |
Combine two functions into a single function so that the rhs
is called on the arguments first,
then the lhs
.
lhs %.% rhs
lhs %.% rhs
lhs |
function to be called second |
rhs |
function to be called first |
a composed function
sq_log <- round %.% sqrt %.% log Match( 10:20, i %fn% (sq_log(i) > 2) -> "big", . -> "small" )
sq_log <- round %.% sqrt %.% log Match( 10:20, i %fn% (sq_log(i) > 2) -> "big", . -> "small" )
Syntactic sugar for creating a single-variable function. Can be conveniently used in Match
statements.
lhs %fn% rhs
lhs %fn% rhs
lhs |
symbol used to denote the function argument |
rhs |
expression that is converted to the function body. |
a function
Match( "abc", is.numeric -> -1, i %fn% grepl("bc", i) -> 0, is.character -> 1 ) print_sq_log <- i %fn% print(sqrt(log(i))) print_sq_log(10)
Match( "abc", is.numeric -> -1, i %fn% grepl("bc", i) -> 0, is.character -> 1 ) print_sq_log <- i %fn% print(sqrt(log(i))) print_sq_log(10)
Returns the value contained inside of an Result or Option Enum or returns if failure.
## S3 method for class 'Result' !x, ... ## S3 method for class 'Option' !x, ...
## S3 method for class 'Result' !x, ... ## S3 method for class 'Option' !x, ...
x |
|
... |
objects to be passed to methods. |
This is similar to unwrap
for Result and Option objects. However,
an Err
or None
variant does not cause execution to stop. Instead, the parent
function immediately returns the Enum intact. Inspired by the ?
operator in Rust.
an object of any class or x
if failure.
!.Result
: Unwrap Result if Ok, otherwise return the Err variant in the parent function.
!.Option
: Unwrap Option if Some, otherwise return the None variant in the parent function.
is_big <- function(x) { if (x > 10) return(Ok(x)) Err("This is small!") } # If 'x' is greater than 10, the value will be printed. # Otherwise, an error is returned. print_big <- function(x) { print(!is_big(x)) }
is_big <- function(x) { if (x > 10) return(Ok(x)) Err("This is small!") } # If 'x' is greater than 10, the value will be printed. # Otherwise, an error is returned. print_big <- function(x) { print(!is_big(x)) }
An object inspired by enums in Rust and types in other functional languages.
Enum(...)
Enum(...)
... |
Symbols specifying the named of the variant, or language call with the names and default values of objects
contained within the variant. Other values can be used as long as the variant is named. The first item in
|
The Enum
function creates a list of objects of class "Enum" *or* functions that generate "Enum" objects
similar to those found in Rust of similar languages. Symbols or characters passed to Enum
become the new
variants. Language objects, i.e. a name followed by parentheses name(...)
, associate the name with the
variant and create a function based on the arguments passed in ...
. When function is called, the
passed arguments are converted into a named list of class "Enum" and associated variant. Like functions, default
values can be given to the variants.
Variants can be assigned specific values using '=
'. For example, Enum( Hello = "world" )
creates
an enum variant named "Hello" with the underlying value of "world"
. If the initial variant is assigned a
single numeric value, then subsequent variants are automatically assigned the next highest value if possible,
similar to using iota()
in Go. Variant names are not allowed to be numeric values or other non-symbolic
values.
a list of variants or variant generators
### Create a Linked List # Node is an enum with two varieties: a link to the next node, and none # 'Node$Some' is a function that accepts two values and generates the enum # variant, while 'Node$Empty' is a variant Node <- Enum( Some(Val, Next), Empty ) # Initialize an empty linked list, push values to the front new_list <- Node$Empty new_list <- Node$Some(1, new_list) new_list <- Node$Some(2, new_list) new_list # return the head of the list ('car') and tail ('cdr') car <- new_list$Val cdr <- new_list$Next ### RGB Colors # The Color enum is provided with a named type "Color". All # variants will have both "Enum" and "Color" as a class. # Each variant is associated with a specific value. Color <- Enum( "Color", Black = c(0,0,0), Red = c(255,0,0), Green = c(0, 255, 0), Blue = c(0, 0, 255), White = c(255, 255, 255) ) Color$Red # This will generate an error since it is not a function # Color$Black() ### Directions # This enum creates a sequence of numbers associated with # a particular direction. Enum automatically increments the # values if the initial variant is assigned a single number Direction <- Enum( North = 1, East, South, West ) # This will result in '5' since North is '1' and West is '4' Direction$North + Direction$West
### Create a Linked List # Node is an enum with two varieties: a link to the next node, and none # 'Node$Some' is a function that accepts two values and generates the enum # variant, while 'Node$Empty' is a variant Node <- Enum( Some(Val, Next), Empty ) # Initialize an empty linked list, push values to the front new_list <- Node$Empty new_list <- Node$Some(1, new_list) new_list <- Node$Some(2, new_list) new_list # return the head of the list ('car') and tail ('cdr') car <- new_list$Val cdr <- new_list$Next ### RGB Colors # The Color enum is provided with a named type "Color". All # variants will have both "Enum" and "Color" as a class. # Each variant is associated with a specific value. Color <- Enum( "Color", Black = c(0,0,0), Red = c(255,0,0), Green = c(0, 255, 0), Blue = c(0, 0, 255), White = c(255, 255, 255) ) Color$Red # This will generate an error since it is not a function # Color$Black() ### Directions # This enum creates a sequence of numbers associated with # a particular direction. Enum automatically increments the # values if the initial variant is assigned a single number Direction <- Enum( North = 1, East, South, West ) # This will result in '5' since North is '1' and West is '4' Direction$North + Direction$West
Return the enumerated type name of an object, if a name was provided.
enum_type(x, ...)
enum_type(x, ...)
x |
Enum object |
... |
objects passed to methods |
character with the name of the enumerated type or NULL
x <- Result$Ok("hello world!") enum_type(x) # "Result"
x <- Result$Ok("hello world!") enum_type(x) # "Result"
Create an Enum variant of Result
used to denote that function contained an error.
This allows the creation of safer functions that do not automatically stop, without using
try
or tryCatch
.
Err(e)
Err(e)
e |
Object to be wrapped in the Enum variant. |
a list with a single value e
and classes "Result} and \code{"Enum
grepl_safe <- function(pattern, x) { if (!is.character(pattern)){ return(Err("'pattern' in 'grepl_safe' was not a character value.")) } if (!is.character(x)){ return(Err("'x' in 'grepl_safe' was not a character value.")) } Ok(grepl(pattern, x)) } #grepl_safe(123, 1:5)
grepl_safe <- function(pattern, x) { if (!is.character(pattern)){ return(Err("'pattern' in 'grepl_safe' was not a character value.")) } if (!is.character(x)){ return(Err("'x' in 'grepl_safe' was not a character value.")) } Ok(grepl(pattern, x)) } #grepl_safe(123, 1:5)
Stop execution of current return expression in Match
, then continue attempting to match conditions.
fallthrough()
fallthrough()
Object of class 'fallthrough'
Match( "abc", is.character -> { print("Found a character.") fallthrough() }, "abc" -> "start of the alphabet", . -> "found nothing" )
Match( "abc", is.character -> { print("Found a character.") fallthrough() }, "abc" -> "start of the alphabet", . -> "found nothing" )
Create an Option
out of an object. By default the object is wrapped in a Some
variant.
Ok
variants of Result
are turned into Some
Options, while Err
variants are
turned into None
Options.
into_option(x, ...)
into_option(x, ...)
x |
Object to be converted |
... |
Objects passed to methods |
an Enum object of class Option
an_error <- Result$Err("hello world!") into_option(an_error) # None
an_error <- Result$Err("hello world!") into_option(an_error) # None
Create a Result
out of an object. By default the object is wrapped in an Ok
variant.
Some
variants of Option
are turned into Ok
Results, while None
variants are
turned into Err
Results
into_result(x, ...)
into_result(x, ...)
x |
Object to be converted |
... |
Objects passed to methods |
an Enum object of class Result
nothing <- Option$None into_result(nothing) # Err
nothing <- Option$None into_result(nothing) # Err
Test whether object has class Enum
.
is.enum(x)
is.enum(x)
x |
object to be tested |
TRUE
if x
is an Enum, FALSE
otherwise
HelloEnum <- Enum( "HelloEnum", Hello, World ) # TRUE is.enum(HelloEnum$Hello) # FALSE is.enum(5)
HelloEnum <- Enum( "HelloEnum", Hello, World ) # TRUE is.enum(HelloEnum$Hello) # FALSE is.enum(5)
Test whether Enum
is also of class type
.
is.enum_type(x, type, ...)
is.enum_type(x, type, ...)
x |
object to be tested |
type |
character string denoting type to check. |
... |
objects passed to methods |
TRUE
if x
has enumerated type type
, FALSE
otherwise
HelloEnum <- Enum( "HelloEnum", Hello, World ) # TRUE is.enum_type(HelloEnum$Hello, "HelloEnum") # FALSE is.enum_type(HelloEnum$Hello, "Hello")
HelloEnum <- Enum( "HelloEnum", Hello, World ) # TRUE is.enum_type(HelloEnum$Hello, "HelloEnum") # FALSE is.enum_type(HelloEnum$Hello, "Hello")
Test whether Result Enum is Ok or an Err.
is.err(x)
is.err(x)
x |
object to be tested |
TRUE
if x
is enumerated type of variant Err
, FALSE
otherwise
sqrt_big <- function(x) { if (x > 1000){ return(Ok(sqrt(x))) } Err("Not large enough!") } x <- sqrt_big(250) is.err(x) # TRUE
sqrt_big <- function(x) { if (x > 1000){ return(Ok(sqrt(x))) } Err("Not large enough!") } x <- sqrt_big(250) is.err(x) # TRUE
Test whether Option Enum is Some or None.
is.none(x)
is.none(x)
x |
object to be tested |
TRUE
if x
is enumerated type of variant None
, FALSE
otherwise
x <- 1:5 get_n <- function(x, n) { if (n > length(x)) return(None) Some(x[n]) } obj <- get_n(x, 6) is.none(obj) # TRUE
x <- 1:5 get_n <- function(x, n) { if (n > length(x)) return(None) Some(x[n]) } obj <- get_n(x, 6) is.none(obj) # TRUE
Test whether Result Enum is Ok or an Err.
is.ok(x)
is.ok(x)
x |
object to be tested |
TRUE
if x
is enumerated type of variant Ok
, FALSE
otherwise
sqrt_big <- function(x) { if (x > 1000){ return(Ok(sqrt(x))) } Err("Not large enough!") } x <- sqrt_big(250) is.ok(x) # FALSE
sqrt_big <- function(x) { if (x > 1000){ return(Ok(sqrt(x))) } Err("Not large enough!") } x <- sqrt_big(250) is.ok(x) # FALSE
Test whether Option Enum is Some or None.
is.some(x)
is.some(x)
x |
object to be tested |
TRUE
if x
is enumerated type of variant Some
, FALSE
otherwise
x <- 1:5 get_n <- function(x, n) { if (n > length(x)) return(None) Some(x[n]) } obj <- get_n(x, 6) is.some(obj) # FALSE
x <- 1:5 get_n <- function(x, n) { if (n > length(x)) return(None) Some(x[n]) } obj <- get_n(x, 6) is.some(obj) # FALSE
Test whether Enum
is variant variant
.
is.variant(x, variant, ...)
is.variant(x, variant, ...)
x |
object to be tested |
variant |
character string denoting variant to check. |
... |
objects passed to methods |
TRUE
if x
is enumerated type of variant variant
, FALSE
otherwise
HelloEnum <- Enum( "HelloEnum", Hello, World ) # TRUE is.variant(HelloEnum$Hello, "Hello") # FALSE is.variant(HelloEnum$Hello, "World")
HelloEnum <- Enum( "HelloEnum", Hello, World ) # TRUE is.variant(HelloEnum$Hello, "Hello") # FALSE is.variant(HelloEnum$Hello, "World")
Functional programming style matching using ->
to separate
conditions from associated return values.
Match(x, ...)
Match(x, ...)
x |
object to match |
... |
conditions used for matching, separated from the returned
value by |
Unlike switch
, Match
accepts a variety of
different condition statements. These can character, numeric,
or logical values, functions, symbols, language objects, enums,
etc. For example, "hello" -> 1
tests whether the object is
equal to "hello"
. If so, the function returns 1
,
otherwise the next condition is tested. <-
can also be used.
If so, the condition & return expression are reversed: 1 <- "hello"
also tests "hello"
and returns 1
.
Each condition is tested sequentially by calling the appropriate method
of match_cond
. If the condition is a character value, then
match_cond.character
is called, and so on. If a match is
confirmed, the right-hand side is evaluated and returned.
For atomic vectors - numeric, logical, or character - Match
will
check for equality. All resulting values must be TRUE
to match.
Lists and environments are checked using identical
. If a
function is placed within the condition, then the function will be evaluated
on object x
. If the result is logical and TRUE
, then it is
considered a match. A non-logical result will be checked again using
match_cond
. Failed function calls with an error are treated
as a non-match rather than stopping Match
. Expressions are evaluated
similar to functions.
The period .
is a special condition in Match
. When alone,
it is treated as the "default" condition that always matches. When used
as a call, though, it matches values within object x
and/or attaches
the individual items within x
for use in the return expression.
For example, x = c(1, 2)
will be matched with the condition
.(1, second)
. This is because the first values are identical
(1 == 1)
. Furthermore, second = 2
for use in the return
expression. Preface a symbol with ..
to evaluate it and check
for equality. ...
can be used to denote any number of unspecified
objects.
The period call .()
can also be used to test named member of x
,
though all objects in .()
must be named to do so. For example, the
condition .(a = 5, b=)
tests whether x
contains "a"
with a value of 5
and "b"
with any value.
If function(...)
is used on the left hand side, then it may
need to be surrounded by parentheses for the parser to properly recognize
it. The %fn%
infix function has be provided as syntactic sugar
for developing functions for matching.
Similar to many functional languages, (first:rest)
can be used
as a condition to extract the first element and the rest from any
vector as long as the vector is sufficiently long. Variables used on the
left hand side can be called on the right hand side expression.
Matching an Enum
causes symbols to represent possible
variants. For example, None -> "none"
would try to match the
variant of x
with None
. If it succeeds, then Match
will return "none"
. A function call on the left-hand side for an
Enum is treated as a variant and its inside arguments, which are made
available in the result expression. So, Some(var) -> sqrt(var)
would
attempt to match on the variant Some
. If it matches, then the
inside is exposed as the variable var
for the right-hand side to
use. The number of objects in the variant on the left-hand side must
match the number of objects inside of x
or else an error will
populate.
Regex conditions can be used when matching strings by surrounding the
expression in braces. For example, the condition "[ab]*" is equivalent
to using grepl("\[ab\]*", ...)
. The braces must be the first and
last characters to trigger a regex match.
Call fallthrough
within a return expression to stop evaluating
the expression and return to matching. This can be convenient for complex
matching conditions or to execute code for side-effects, such as printing.
an object based on the matched clause. An Error is produced if no match is found.
## Matching to functions, characters, regex, and default Match( "abc", is.numeric -> "Not a character!", is.character -> { print("Found a character!") fallthrough() }, "a" | "b" | "c" -> "It's a letter!", "{bc}" -> "Contains 'bc'!", . -> "Can be anything!" ) ## Unwrapping a Result enum val <- Result$Ok("hello world!") Match( val, Ok(w) -> w, Err(s) -> s ) ## Using functions # If 'function' is used on the lhs, surround in '()' # Alternatively, use %fn% notation Match( 1:10, (function(i) mean(i) < 5) -> TRUE, i %fn% (mean(i) >= 5) -> FALSE ) ## Extracting parts x <- list(a = 5, b = 6, c = 7) Match( x, .(a=, d=2) -> "won't match, no 'd'", .(a=5, b=) -> "will match, a == '5'", (x:xs) -> { print(x) # 5 print(xs) # list(b=6, c=7) "will match, since not empty" }, . -> "this matches anything!" ) z <- c(1,2,3,4) first <- 1 Match( z, .(0, ...) -> "no match, first is 1 not 0", .(1, 2) -> "no match, z has 4 elements", .(x, 2, ...) -> paste("match, x = ", x), .(..first, ...) -> "match, since 'first' == 1" )
## Matching to functions, characters, regex, and default Match( "abc", is.numeric -> "Not a character!", is.character -> { print("Found a character!") fallthrough() }, "a" | "b" | "c" -> "It's a letter!", "{bc}" -> "Contains 'bc'!", . -> "Can be anything!" ) ## Unwrapping a Result enum val <- Result$Ok("hello world!") Match( val, Ok(w) -> w, Err(s) -> s ) ## Using functions # If 'function' is used on the lhs, surround in '()' # Alternatively, use %fn% notation Match( 1:10, (function(i) mean(i) < 5) -> TRUE, i %fn% (mean(i) >= 5) -> FALSE ) ## Extracting parts x <- list(a = 5, b = 6, c = 7) Match( x, .(a=, d=2) -> "won't match, no 'd'", .(a=5, b=) -> "will match, a == '5'", (x:xs) -> { print(x) # 5 print(xs) # list(b=6, c=7) "will match, since not empty" }, . -> "this matches anything!" ) z <- c(1,2,3,4) first <- 1 Match( z, .(0, ...) -> "no match, first is 1 not 0", .(1, 2) -> "no match, z has 4 elements", .(x, 2, ...) -> paste("match, x = ", x), .(..first, ...) -> "match, since 'first' == 1" )
Called by Match
the check whether a condition matches. Used to create custom methods for matching.
match_cond(cond, x, do, ...)
match_cond(cond, x, do, ...)
cond |
match condition |
x |
object being matched |
do |
return expression associated with the condition. If |
... |
arguments passed to evaluation |
See the Match
details for explanations about provided methods.
FALSE
if no match, or a list containing TRUE
and the evaluated expression
Applies Match
to each individual object within the input rather than matching the entire object.
Matchply(x, ...)
Matchply(x, ...)
x |
a vector (including list) or expression object |
... |
conditions and expressions for matching. See |
See Match
for details on condition implementation. Default conditions using the
period .
are highly recommended to prevent error.
Matchply
is a wrapper to lapply
and sapply
, depending on the input object,
with ...
converted to a match statement for easy use.
vector depending on input x
. By default, sapply
is
used with simplify = TRUE
. This could return a vector, matrix, list,
etc. When simplify = FALSE
or a list is provided, the result will be
a list.
new_list <- list( hello = "World!", nice = 2, meet = "u" ) Matchply( new_list, is.numeric -> "found a number!", "{rld}" -> "maybe found 'World'!", "u" | "z" -> "found a letter", . -> "found nothing" )
new_list <- list( hello = "World!", nice = 2, meet = "u" ) Matchply( new_list, is.numeric -> "found a number!", "{rld}" -> "maybe found 'World'!", "u" | "z" -> "found a letter", . -> "found nothing" )
An Enum variant of Option
used to denote that a function returned
no value.
None
None
an empty list of classes "Option"
and "Enum"
Create an Enum variant of Result
used to denote that function did not contain an error.
This allows the creation of safer functions.
Ok(x)
Ok(x)
x |
Object to be wrapped in the Enum variant. |
a list with a single value x
and classes "Result} and \code{"Enum
grepl_safe <- function(pattern, x) { if (!is.character(pattern)){ return(Err("'pattern' in 'grepl_safe' was not a character value.")) } if (!is.character(x)){ return(Err("'x' in 'grepl_safe' was not a character value.")) } Ok(grepl(pattern, x)) } #grepl_safe(123, 1:5)
grepl_safe <- function(pattern, x) { if (!is.character(pattern)){ return(Err("'pattern' in 'grepl_safe' was not a character value.")) } if (!is.character(x)){ return(Err("'x' in 'grepl_safe' was not a character value.")) } Ok(grepl(pattern, x)) } #grepl_safe(123, 1:5)
An Enum that mimics Rust's "Option" type. This is used to denote whether
a function returned an object or not, rather than returning NULL
.
Option
Option
list with 1 Enum generators and 1 Enum variant
Wrap x
in the 'Some' variant.
Variant denoting that nothing was returned.
An Enum that mimics Rust's "Result" type. This is used to denote whether a function contained an error without stopping execution and allowing the error result to be unwrapped.
Result
Result
list with 2 Enum generators
Wrap x
in the 'Ok' variant.
Wrap x
in the 'Err' variant.
Create an Enum variant of Option
used to denote that function returned a value.
This allows the creation of safer functions that extract values from other objects, without using
try
or tryCatch
.
Some(x)
Some(x)
x |
Object to be wrapped in the Enum variant. |
a list with a single value x
and classes "Option} and \code{"Enum
subset_safe <- function(x, index) { if (index > length(x)){ return(None) } Some(x[index]) }
subset_safe <- function(x, index) { if (index > length(x)){ return(None) } Some(x[index]) }
Evaluates given expression returning an Err
Result if there is an error, otherwise an Ok
Result.
Try(expr)
Try(expr)
expr |
expression to evaluate |
Result Enum of variant Ok
or Err
# This returns an Err Try(sqrt + 1) # This returns an Ok Try(sqrt(5) + 1)
# This returns an Err Try(sqrt + 1) # This returns an Ok Try(sqrt(5) + 1)
Returns the value contained inside of an enum variant. The function strips all relevant attributes from the object, returning its bare value.
unwrap(x, ...) unwrap_or(x, alt, ...)
unwrap(x, ...) unwrap_or(x, alt, ...)
x |
Enumerated value to unwrap |
... |
objects to be passed to methods. |
alt |
Alternative value to be returned in case of failure |
unwrap
is used to extract the inside objects of an Enum. Unless the Enum was assigned a specific
value, the returned value will be a list with names equal to those in the Enum declaration.
Result
and Option
have associated unwrap
methods that automatically
call an error and stop execution if the variant is either Err(e)
or None
, respectively.
unwrap_or
allows the user to specify an alternative value in case of failure on the part of
Result
or Option
.
an object of any class.
unwrap_or
: Extract the inside of Enum. If variant is 'Err' or 'None', the alternative is returned.
Color <- Enum( "Color", Black = c(0,0,0), Red = c(255,0,0), Green = c(0, 255, 0), Blue = c(0, 0, 255), White = c(255, 255, 255) ) red_rgb <- unwrap(Color$Red) blue <- rev(red_rgb) blue new_err <- Err("hello world!") unwrap_or(new_err, "this is not an error")
Color <- Enum( "Color", Black = c(0,0,0), Red = c(255,0,0), Green = c(0, 255, 0), Blue = c(0, 0, 255), White = c(255, 255, 255) ) red_rgb <- unwrap(Color$Red) blue <- rev(red_rgb) blue new_err <- Err("hello world!") unwrap_or(new_err, "this is not an error")
Return the variant name of an enumerated type.
variant(x, ...)
variant(x, ...)
x |
Enum object |
... |
objects passed to methods |
character with the name of the variant or NULL
x <- Result$Ok("hello world!") variant(x) # "Ok"
x <- Result$Ok("hello world!") variant(x) # "Ok"