Skip to content

Instantly share code, notes, and snippets.

@Yunuuuu
Last active April 3, 2024 03:35
Show Gist options
  • Select an option

  • Save Yunuuuu/f5450d1d58e680cc4ba79dafcce636ae to your computer and use it in GitHub Desktop.

Select an option

Save Yunuuuu/f5450d1d58e680cc4ba79dafcce636ae to your computer and use it in GitHub Desktop.
Use environment to define internal namespace and sub-namespace like Python. Instead of using `R6` object, which will make the data object mutable, this will break normal scoping rules of R.
# This is not same with `r-polars` pacakge, which use more complicated design to create namespace and sub-namespace
#
# In package namespace -----------------------
utils::globalVariables("self")
# Check that all elements of a list are named.
# NULL and empty lists return TRUE.
all_named <- function(x) {
if (length(names(x)) != length(x) || any(names(x) == "")) {
return(FALSE)
}
TRUE
}
# Check that all elements of a list are functions
all_functions <- function(x) all(vapply(x, is.function, logical(1L)))
# create New namespace, which is just an environment ------------
# 1. For every new namespace, we must add a new S3 method `$.ClassName` and
# `[[.ClassName` to bind the namespace into the special class
# (dispatch_method).
# 2. We can use `self` to refer the input data (other methods or fields can
# be directly acquired by `self$`)
# 3. For a sub-namespace, we must define an active function to change the
# underlying class of the data value in the parent namepace. Then add a new
# S3 method `$.ClassName` and `[[.ClassName` to bind the sub-namespace into
# the new class
# 4. we can also check the data value match a special data type for the
# sub-namespace
new_namespace <- function(public = list(), active = list()) {
if (!all_named(public) || !all_named(active)) {
stop("All elements of public, and active must be named.")
}
allnames <- c(names(public), names(active))
if (anyDuplicated(allnames)) {
stop("All items in public, and active must have unique names.")
}
if (any("self" == allnames)) {
stop("Items cannot use reserved names 'self'.")
}
if (!all_functions(public)) {
stop("All items in public must be functions.")
}
if (!all_functions(active)) {
stop("All items in active must be functions.")
}
namespace <- new.env(parent = emptyenv())
namespace$public <- new.env(parent = emptyenv())
namespace$active <- new.env(parent = emptyenv())
list2env(public, namespace$public)
list2env(active, namespace$active)
namespace
}
# function used to dispatch methods defined in `namespace` to a special class
# function used to create `$.ClassName` and `[[.ClassName` method
dispatch_method <- function(.__class__, .__namespace__) {
# we use special name to prevent override other function (also known as private methods) defined this package
force(.__class__)
force(.__namespace__)
function(self, .__name__) {
# environment used to find `self`, `self` should be the data value
# the parent environment of this environment is the package namespace
# it's safe to just change the function environment in this package
force(self)
.__enclos_env__ <- environment()
.__public_env__ <- .subset2(.__namespace__, "public")
.__active_env__ <- .subset2(.__namespace__, "active")
if (exists(.__name__, envir = .__public_env__, inherits = FALSE)) {
.__fn__ <- .subset2(.__public_env__, .__name__)
environment(.__fn__) <- .__enclos_env__
.__fn__
} else if (
exists(.__name__, envir = .__active_env__, inherits = FALSE)) {
.__fn__ <- .subset2(.__active_env__, .__name__)
environment(.__fn__) <- .__enclos_env__
makeActiveBinding(".__active_fn__", .__fn__, .__enclos_env__)
.__active_fn__ # nolint
} else {
stop(sprintf(
"No method `%s()` found for Class `%s`", .__name__, .__class__
))
}
}
}
# Let's do some examples ----------------------------
SeriesNameSpace <- new_namespace(
public = list( # Methods used by user
slice = function(offset, length = NULL) { # 0-based index
if (is.null(length)) end <- self$len else end <- offset + length
self[offset:end]
}
),
active = list( # fields (data property value) should be an active binding
name = function() {
attr(self, "name")
},
len = function() {
length(self)
},
dtype = function() {
typeof(self)
},
struct = function() {
if (!all_named(self)) {
stop("data must be a `struct` data") # I suppose `struct` data is just a named series
}
class(self) <- "Series_Struct"
invisible(self)
}
)
)
`$.Series` <- dispatch_method("Series", SeriesNameSpace)
`[[.Series` <- `$.Series`
# add a function to create the Series object
new_series <- function(value, name = "", ...) {
structure(value, name = name, ..., class = "Series")
}
# For a sub-namespace, we just change the underlying class of the data value in
# the parent-namespace active field.
# Then add a new S3 method `$.ClassName` and `[[.ClassName` to bind the
# sub-namespace into the new class
StructNameSpace <- new_namespace(
public = list(),
active = list(
fields = function() names(self)
)
)
# It's better to add a new `print` method for the sub-namespace class,
# It's more useful to show the methods defined in this Namespace
# Here, we just print the namespace environment
print.Series_Struct <- function(x) {
print(StructNameSpace)
invisible(x)
}
`$.Series_Struct` <- dispatch_method("Series_Struct", StructNameSpace)
`[[.Series_Struct` <- `$.Series_Struct`
simple_series <- new_series(1:10)
simple_series$len
simple_series$dtype
simple_series$name
simple_series$slice(6L)
simple_series$slice(0)
# should be an error, as `simple_series` is not `struct` object
simple_series$struct
struct_series <- new_series(1:2, names = letters[1:2])
struct_series$struct
struct_series$struct$fields
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment