4

This should be really basic but I'm totally new to defining functions in R.

Sometimes I want to define a function which simply consists of wrapping a base function in one or more other functions.

E.g., I wrote prop.table2 which basically accomplishes prop.table(table(...)).

The hitch that I see is that I also want my wrapper function to take the optional arguments of any of the subfunctions and pass them appropriately,

E.g.,

prop.table2(TABLE, useNA = "always", margin = 2) =
  prop.table(table(TABLE, useNA = "always"), margin = 2)

What's the simplest way to accomplish something like this (assuming there's no conflict in argument names, etc)? My baseline approach is to simply paste all of the optional arguments to each subfunction into the main function definition, i.e., defining:

prop.table2 <- function(..., exclude = if (useNA == "no") c(NA, NaN),
                        useNA = c("no", "ifany", "always"), dnn = list.names(...),
                        deparse.level = 1, margin = NULL)

Let's work from this example for concreteness:

dt <- data.table(id = sample(5, size = 100, replace = TRUE),
                 grp = letters[sample(4, size = 100, replace=TRUE)])

I want to replicate the following with my function:

dt[ , prop.table(table(grp, id, useNA = "always"), margin = 1)]

      id
grp             1          2          3          4          5       <NA>
  a    0.28571429 0.10714286 0.17857143 0.25000000 0.17857143 0.00000000
  b    0.12000000 0.28000000 0.08000000 0.12000000 0.40000000 0.00000000
  c    0.23076923 0.23076923 0.15384615 0.19230769 0.19230769 0.00000000
  d    0.23809524 0.19047619 0.23809524 0.28571429 0.04761905 0.00000000
  <NA>    

Here's where I'm at now, which still doesn't quite work; the idea was to split everything into those arguments which prop.table accepts and then pass the rest to table, but I'm still struggling.

prop.table2 <- function(...) {
  dots <- list(...)
  dots2 <- dots
  dots2[intersect(names(dots2), names(formals(prop.table)))] <- NULL
  dots3 <- dots2
  dots3[intersect(names(dots3), names(formals(table)))] <- NULL
  dots2[names(dots2) == ""] <- NULL
  prop.table(table(dots3, dots2), margin = list(...)$margin)
}
                                                          
MichaelChirico
  • 33,841
  • 14
  • 113
  • 198
  • you could use a list and do.call, `function(..., prop.param = list()) do.call(prop.table, c(table(...), prop.param))` – baptiste May 06 '15 at 19:50
  • It would get complicated if the sub functions share parameter names. You are safest controling the passing yourself, but here's a question that did inspect the formals of a function to see which parameters to pass along: http://stackoverflow.com/questions/25749661/how-to-pass-the-parameters-in-the-parent-function-to-its-two-children-func/25750688#25750688 – MrFlick May 06 '15 at 21:37
  • @baptiste is there a typo? That's not working for me, e.g.: `dt<-data.table(id=sample(10,size=100,rep=T),grp=letters[sample(10,size=100,rep=T)]); dt[,prop.table2(id,grp,prop.param=list(margin=1))]` – MichaelChirico May 07 '15 at 00:20
  • @MrFlick inspired by your other solution I thought something along the lines of `prop.table2<-function(...){prop.table(table(...),margin=list(...)$margin)}` would work, but I can't seem to finagle it--the problem seems to be in passing "..." to `table` – MichaelChirico May 07 '15 at 00:32
  • @MichaelChirico When you have `table(...)`, that's passing everything to table. You can't easily split the `...`s. It's all or nothing. – MrFlick May 07 '15 at 00:34

2 Answers2

2

You may use a functional with unspecified arguments (...). A functional is a higher order function that accepts a function as an argument (eg lapply()).

prop.table2 <- function(f, ...) {
  f(...)
}

a <- rep(c(NA, 1/0:3), 10)
table(round(a, 2), exclude = NULL)
#0.33  0.5    1  Inf <NA> 
#  10   10   10   10   10 

prop.table2(table, round(a, 2), exclude = NULL)
#0.33  0.5    1  Inf <NA> 
#  10   10   10   10   10 

@ MichaelChirico

Sorry that below is as much as I can think of at the moment.

A composite function is created, compose(), and the margin argument of prop.table() should be determined in it.

Specific functions (f and g) are added in prop().

Then additional arguments of table() can be added.

Note that, due to missing values, it'll cause an error if margin is set to be 2 as your example.

a <- rep(c(NA, 1/0:3), 10)

compose <- function(f, g, margin = NULL) {
    function(...) f(g(...), margin)
}
prop <- compose(prop.table, table)
prop(round(a, 2), exclude = NULL)

# 0.33  0.5    1  Inf <NA> 
# 0.2  0.2  0.2  0.2  0.2 

@MichaelChirico

Below is the second edit.

library(data.table)
set.seed(1237)
dt <- data.table(id=sample(5,size=100,replace=T),
                 grp=letters[sample(4,size=100,replace=T)])

compose <- function(f, g, margin = 1) {
    function(...) f(g(...), margin)
}
prop <- compose(prop.table, table)

dt[,prop(grp, id, useNA="always")]

#id
#grp           1          2          3          4          5       <NA>
#a    0.23529412 0.17647059 0.11764706 0.23529412 0.23529412 0.00000000
#b    0.11764706 0.29411765 0.05882353 0.17647059 0.35294118 0.00000000
#c    0.11538462 0.19230769 0.30769231 0.15384615 0.23076923 0.00000000
#d    0.34782609 0.13043478 0.13043478 0.17391304 0.21739130 0.00000000
#<NA>
Jaehyeon Kim
  • 1,328
  • 11
  • 16
  • I'm not sure how this solves the problem... 1) I'm writing the new function to avoid having to rewrite the names of all the sub-functions every time and 2) you're not using `prop.table`. – MichaelChirico May 07 '15 at 00:13
  • I think we're off on what I'm going for--what your code seems tailored to is writing a _general_ function designed to compose any two _given_ functions; instead, what I have in mind is a _specific_ function which is shorthand for the composition of two _specific_ functions. – MichaelChirico May 08 '15 at 00:30
  • I consider `prop()` is a proper shorthand of the two functions. 1) By `compose()`, the only extra argument of `prop.table()`, can be specified, which is `margin`. 2) By `prop()`, any argument of `table()` can be specified. It is up to you whether to adjust some arguments of `table()` and, if nothing is adjusted, default values will just be taken. In this way, you don't need to bring all optional arguments or arguments with default values into your function. To me, `prop()` is specific enough. – Jaehyeon Kim May 08 '15 at 00:53
  • This helps me, thank you – cloudscomputes May 08 '18 at 02:53
1

I was missing a list() in my earlier comment, the following should work,

prop.table2 <- function(..., prop.param = list()) 
                 do.call(prop.table, c(list(table(...)), prop.param))

# with the example provided
library(data.table)
dt <- data.table(id=sample(5,size=100,replace=T),
                 grp=letters[sample(4,size=100,replace=T)])
dt[,prop.table2(grp,id,useNA="always",prop.param=list(margin=1))]
      id
grp             1          2          3          4          5       <NA>
  a    0.10714286 0.28571429 0.14285714 0.25000000 0.21428571 0.00000000
  b    0.09090909 0.18181818 0.30303030 0.15151515 0.27272727 0.00000000
  c    0.38095238 0.14285714 0.19047619 0.09523810 0.19047619 0.00000000
  d    0.11111111 0.22222222 0.44444444 0.16666667 0.05555556 0.00000000
  <NA> 

Edit: the OP suggests this modification based on previous answers to filter ... based on their names,

prop.table2 <- function(...){
  dots <- list(...)
  passed <- names(dots)
  # filter args based on prop.table's formals
  args <- passed %in% names(formals(prop.table))
  do.call('prop.table', c(list(do.call('table', dots[!args])), 
          dots[args]))
}

# with the example provided
library(data.table)
dt <- data.table(id=sample(5,size=100,replace=T),
                 grp=letters[sample(4,size=100,replace=T)])
dt[,prop.table2(grp,id,useNA="always",margin=1)]
      id
grp             1          2          3          4          5       <NA>
  a    0.10714286 0.28571429 0.14285714 0.25000000 0.21428571 0.00000000
  b    0.09090909 0.18181818 0.30303030 0.15151515 0.27272727 0.00000000
  c    0.38095238 0.14285714 0.19047619 0.09523810 0.19047619 0.00000000
  d    0.11111111 0.22222222 0.44444444 0.16666667 0.05555556 0.00000000
  <NA> 
Community
  • 1
  • 1
baptiste
  • 75,767
  • 19
  • 198
  • 294
  • Still not quite working. I think the problem is you're not passing arguments carefully to the second subfunction; I've added a working example to the body of the question so we're on the same footing about what to aim for. – MichaelChirico May 07 '15 at 00:59
  • dunno, as far as i can tell it works – baptiste May 07 '15 at 07:13
  • maybe I'm using it wrong, could you paste a code snippet that's running? – MichaelChirico May 07 '15 at 12:35
  • the above already contains your example and its expected result – baptiste May 07 '15 at 20:17
  • sorry, i must have missed this in my morning grog. i'll double-check this when i get back to a computer with a compiler. – MichaelChirico May 07 '15 at 20:18
  • OK, got it. I'm still not satisfied. How would this generalize to, say, `f<-function(allparams){g(h(i(...,iparams),hparams),gparams)}`? Also, I'd prefer to write f(x,y,z) instead of having to name `prop.param` (or anything shorter), as it feels more natural. – MichaelChirico May 08 '15 at 00:26
  • 1
    this question has been asked _many_ times, I was just a bit too lazy to search for duplicates. [this answer](http://stackoverflow.com/questions/4124900/is-there-a-way-to-use-two-statements-in-a-function-in-r) might be closer to what you want – baptiste May 08 '15 at 00:30
  • there's still a problem, in that the main argument of `table` is `...`; it's still not clear to me how to pass that through _and_ match the optional `table` arguments. – MichaelChirico May 08 '15 at 00:48
  • @MichaelChirico you should probably post your own answer, since your edit changes this one substantially – baptiste May 08 '15 at 01:23
  • OK; I thought it was close enough to edit yours and give you credit for the answer, considering 95% did come from your suggestions. Thank you by the way. – MichaelChirico May 08 '15 at 01:24
  • Hmm unfortunately I can't seem to add any new answers while it's marked as duplicate. – MichaelChirico May 08 '15 at 01:33
  • didn't think of that, sorry. I've added a version of your suggested code – baptiste May 08 '15 at 02:06
  • ah even better. I knew there was a way to skip the `dots2` step... – MichaelChirico May 08 '15 at 02:51