classes/attributes of functions ?

functions
#1

These are almost never used it seems. One exception I can think about are magrittr's functional sequences, which have a fseq class and a printing method.

I'm curious if the subject has been approached already.

For example functions which support grouping, or quasiquotation could be tagged as such. Adverbs could be tagged, deprecated functions could be tagged, type stable functions could be tagged.

As far as I can tell this type of information is largely unstructured so far (e.g. it is probably in the doc).

What would be pros and cons ? Any examples of packages setting attributes on their functions (or functions created by their functions) ?

0 Likes

#2

Classes are only worthwhile if there's code using it. For fseq objects, the class helps by giving a nice printing method and allowing subsetting. For categorizing functions, I think documentation is good enough.

But if you have something in mind, a rough example would be interesting.

For identifying deprecated functions, I'd rather see subclasses for errors and warnings. Something similar to Python. Then we could write code like this:

tryCatch(
  1:2 + 1:3,
  warning = function(w) {
    # Mismatched lengths are unacceptable!
    if (inherits(w, "coprimeLengthsWarning")) {
      stop(conditionMessage(w))
    }
  }
)
0 Likes

#3

I just noticed that purrr::compose also returns a function with a class purrr_function_compose, but it keeps the class function

0 Likes

#5

I found another case of functions having another class : rlang::as_function , when given, a formula argument, it returns a rlang_lambda_function object. Unlike in magrittr, the function class is removed altogether. But I don't know if this is used by code or not.

You make a good point and it might be an XY problem, I mostly want structured documentation.

I think it's a bummer though that in a language where everything is a structured object, documentation is not at all. rdocumentation are in a unique position to be able to provide this but they don't seem to share my enthusiasm.

My question was general and largely out of curiosity but I'm also thinking a lot about adverbs these days, for example, how would you answer the following question :

What are the adverbs in base R ?

Negate() is documented next to Reduce() and Filter(), the term adverb didn't exist, if I want to find such functions the only way I can think about is by running all the example of base packages, parsing them to see where there are simple calls of a given function returning a function, I could also check the Value field for the term "function" to get some candidates. That's messy!

An attribute for such type table functions would be handy in this case. I get your point about attributes existing for code, not to fix the doc however. But it CAN be used in code. If I use an adverb as the .f parameter of map_chr it could fail right away with an explicit message :

map_chr(c("foo","bar"), Negate)
#> Negate is an adverb and its output is not coercible to character, which map_chr requires

Instead I get

map_chr(c("foo","bar"), Negate)
#> Error in get(as.character(FUN), mode = "function", envir = envir) : 
#>   object 'foo' of mode 'function' was not found

The above operation will always fail, but the information I use to say that, though simple to define and objective, is not structured anywhere. Errors are right in the gray area between code and documentation, here we'd gain clearer errors AND ressources.

1 Like

#6

just another quick example that comes to mind is asserthat which lets you add attributes to functions to construct meaningful error messages. it's a cool principle that somehow sadly never took fully of.

1 Like

#7

you can skip the whole inherits part, tryCatch already works with custom condition classes, so tryCatch(..., MyFancyWarning= function (... ))should work

2 Likes

#8

That's really cool! Thanks for pointing this out.

deprecationWarning <- function(message, call = NULL) {
  warn <- simpleWarning(message = message, call = call)
  class(warn) <- c("deprecationWarning", "warning", "condition")
  warn
}

# Verbose adaptation of .Deprecated
deprecated <- function(new,
                       package = NULL,
                       msg,
                       old = as.character(sys.call(sys.parent()))[1L]) {
  msg <- if (missing(msg)) {
    msg <- gettextf("'%s' is deprecated.\n", old)
    if (!missing(new)) {
      msg <- c(msg, gettextf("Use '%s' instead.\n", new))
    }
    pkg_msg <- if (!is.null(package)) {
      gettextf("See help(\"Deprecated\") and help(\"%s-deprecated\").", package)
    } else {
      gettext("See help(\"Deprecated\")")
    }
    c(msg, pkg_msg)
  } else {
    as.character(msg)
  }
  warn <- deprecationWarning(message = msg)
  warning(warn)
}

old_junk <- function() {
  deprecated("new_junk")
}

tryCatch(
  old_junk(),
  deprecationWarning = function(warn) {
    message(
      "Caught the deprecation warning!\n",
      conditionMessage(warn)
    )
  }
)
# Caught the deprecation warning!
# 'old_junk' is deprecated.
# Use 'new_junk' instead.
# See help("Deprecated")

Now I feel like making a package with factory functions for creating new condition classes.

0 Likes

#9

An example where I found custom error classes useful is when working with web APIs. There are some errors where you want to retry your request (time-outs and such) and others where you just want to skip and continue with the next requests.

1 Like

#10

If you are interested in these ideas, I'd highly reading about vctrs.

1 Like

#11

Thanks, I had skimmed it already but I'll take a look more in depth!

0 Likes

#12

@nwerth since you asked if I had something in mind, I was working on this : https://github.com/moodymudskipper/tags .

It's a collection of experiments where I've mostly given a class "tag" to functions to be able to call them by using $.tag, I've also given some of them a class "adverb" that I plan to use to be able to compose adverbs.

For example you can make any function compatible with quasiquotation by calling ..bang$fun(...)

library(tags)
library(rlang)
u <- "speed"
v <- quote(dist)
w <- quo(time)
x <- list(a=c(1, 2), b=c(3, 4))
..bang$transform(head(cars,2), !!w := !!v / !!sym(u), !!!x)
#>   speed dist time a b
#> 1     4    2  0.5 1 3
#> 2     4   10  2.5 2 4
0 Likes

#13

That is definitely interesting. I like the idea of code with higher-order functions looking more like namespaces. I can imagine modified functions speaking "dialects" of their original selves, based on the environment they come from. And it's great how easy it is for users to make their own adverbs.

Names starting with periods are sometimes used to mark internal variables in a package. The R documentation acknowledges the practice but never encourages or discourages it. Still, it might be better to replace the .. prefix with something like tag_*. Users can always avoid namespace clashes in their own scripts with the tags:: notation. (The short package name makes this even nicer.)

Have you used the package in analysis code yet?

0 Likes

#14

Thanks a lot for your feedback.

About naming I thought also about non syntactic names, the above would become something like `#bang`$transform(...), it looks strange but maybe it's a good thing . It disables the magical autocomplete for some reason though.

The package is very young (well a few hours old :slight_smile: ), and the ideas not old either too, so it's not tested much and I've not done any complete project using it.

0 Likes