<- array(1:20, c(4, 5))
x <- array(1:5 * 100, c(1, 5))
y <- array(20:1, c(4, 5))
z print(x)
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 1 5 9 13 17
#> [2,] 2 6 10 14 18
#> [3,] 3 7 11 15 19
#> [4,] 4 8 12 16 20
print(y)
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 100 200 300 400 500
print(z)
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 20 16 12 8 4
#> [2,] 19 15 11 7 3
#> [3,] 18 14 10 6 2
#> [4,] 17 13 9 5 1
Operator Overloading
1 Introduction
Sometimes broadcasting is needed in a large mathematical expression, involving multiple variables, where precedence is of importance. For example in an expression like this:
x / (y + z) + a
Using the bc.*
functions for that, while possible, may be inconvenient. It may be more convenient to use the base operators directly, whilst still keeping the broadcasting property.
To this end, the ‘broadcast’ package allows the user to overload the base operators. This includes mathematical operators (+, -., *, /, etc.), Boolean operators (&, |), bit-wise operators (&, |), and relational operators (==, !=, etc.).
‘broadcast’ provides 2 ways to overload operators:
- Via the bc_chain() function, to evaluate a mathematical expression using overloaded operators for broadcast support.
- Via the broadcaster class, which comes with its own method dispatch for the base operators.
2 Example
Consider the matrices x
, y
and z
:
Suppose we wish to compute x + y / z
;
The following is wrong:
bc.num(x, y, "+") |> bc.num(y, "/")
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 1.01 1.025 1.030000 1.0325 1.034
#> [2,] 1.02 1.030 1.033333 1.0350 1.036
#> [3,] 1.03 1.035 1.036667 1.0375 1.038
#> [4,] 1.04 1.040 1.040000 1.0400 1.040
…because division must come before addition, according to standard math precedence rules.
We could do the following:
bc.num(y, z, "/") |> bc.num(x, "+")
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 6.000000 17.50000 34.00000 63.00000 142.0000
#> [2,] 7.263158 19.33333 37.27273 71.14286 184.6667
#> [3,] 8.555556 21.28571 41.00000 81.66667 269.0000
#> [4,] 9.882353 23.38462 45.33333 96.00000 520.0000
…but as the mathematical expression involves more and more variables with a greater variety of operators, it comes more and more taxing to keep track of the operator precedence.
Luckily, ‘broadcast’ provides 2 alternative ways.
One is to evaluate the expression using a formula:
bc_chain(~ x + y / z)
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 6.000000 17.50000 34.00000 63.00000 142.0000
#> [2,] 7.263158 19.33333 37.27273 71.14286 184.6667
#> [3,] 8.555556 21.28571 41.00000 81.66667 269.0000
#> [4,] 9.882353 23.38462 45.33333 96.00000 520.0000
The other, perhaps more natural way, is to designate all arrays as a “broadcaster”; all base operators that involve a broadcaster will use the overloaded variant provided by the ‘broadcast’ package - like so:
broadcaster(x) <- TRUE
broadcaster(y) <- TRUE
broadcaster(z) <- TRUE
+ y / z
x #> [,1] [,2] [,3] [,4] [,5]
#> [1,] 6.000000 17.50000 34.00000 63.00000 142.0000
#> [2,] 7.263158 19.33333 37.27273 71.14286 184.6667
#> [3,] 8.555556 21.28571 41.00000 81.66667 269.0000
#> [4,] 9.882353 23.38462 45.33333 96.00000 520.0000
#> broadcaster
Actually, not all arrays necessarily need to be a broadcaster.
Overloaded operations that involve a broadcaster, will result in a broadcaster.
So technically, one could just make the array that is used first - according to the mathematical rules of precedence - as a broadcaster, and broadcasting will continue to be used.
In this case, the mathematically first preceding array is y
.
To demonstrate, we’ll un-set x
and z
as broadcaster, and you’ll see broadcasting still occurs:
broadcaster(x) <- FALSE
broadcaster(z) <- FALSE
+ y / z
x #> [,1] [,2] [,3] [,4] [,5]
#> [1,] 6.000000 17.50000 34.00000 63.00000 142.0000
#> [2,] 7.263158 19.33333 37.27273 71.14286 184.6667
#> [3,] 8.555556 21.28571 41.00000 81.66667 269.0000
#> [4,] 9.882353 23.38462 45.33333 96.00000 520.0000
#> broadcaster
3 Overload method tables
The overloaded operators from the ‘broadcast’ package attempts to mimic the base operators accurately, except that broadcasting is used (obviously).
The following tables overview the behaviour of the operators.
3.1 Regular Arithmetic operators
If both sides of the operators given here are numeric or logical, the following holds:
operator | action | function call |
---|---|---|
+ | add | bc.d |
- | substract | bc.d |
* | multiply | bc.d |
/ | divide | bc.d |
^ | raise to power | bc.d |
%/% | floored integer divide | bc.i |
%% | modulo | bc.i |
3.2 Complex Arithmetic operators
If at least one of the arguments of the operators given here are complex, the following holds:
operator | action | function call |
---|---|---|
+ | add | bc.cplx |
- | substract | bc.cplx |
* | multiply | bc.cplx |
/ | divide | bc.cplx |
3.3 Logical Operators
If both sides of the operators given here are complex, numeric or logical, the following holds:
operator | action | function call |
---|---|---|
& | and | bc.b |
| | or | bc.b |
3.4 Bit-wise operators
If both sides of the operators given here are type of raw, the following holds:
operator | action | function call |
---|---|---|
& | bit-wise and | bc.bit |
| | bit-wise or | bc.bit |
3.5 Relational operators
For relational operators, ‘broadcast’ first finds the “highest” (or most complex) atomic type of both sides, coerces both sides to said type, and then performs the broadcasted relational operation. Recursive types are not supported.
The types, from complex to simple, are: character
, complex
, numeric
(also known as double
), integer
, logical
, and raw
.
4 Derived operators
There is no overload for xor()
, as xor()
is defined using the existing base relational and logical (or bit-wise, for type of raw
) operators.
There is also no overload for the %d==%, %d!=%, etc. operators, as they too are defined using the existing base relational and logical operators.
5 Base operators vs Overloaded operators
Although the operators overloaded by ‘broadcast’ attempt to mimic the base operators accurately, the are a few differences.
Most notably, the broadcasted operators do not preserve attributes.
Broadcasting often results in an object with more dimensions, larger dimensions, and/or larger length than the original objects.
Therefore, the names
, dimnames
, and dim
attributes often no longer fit the new object.
Moreover, class attributes are related to dimensions or length.
For example, the matrix
class presumes the object to have 2 dimensions, and the various classes from the ‘bit’ package use length-related attributes for their functionality.
So even class attributes cannot be guaranteed to hold for the resulting objects.
Only some class attributes, like the ‘broadcaster’ class (and related) attributes, will be preserved, if present.
6 Overloaded operators vs bc.*
functions
Overloading the operators is primarily useful for encuring correct mathematical precedence, and to reduce the amount of typing.
However, the many bc.*
functions provide much greater control than the simple operators can provide.
For example:
The &
and |
operators provide logical AND and OR, respectively for all atomic types except type raw
; for type raw
, the &
and |
operators provide BIT-WISE AND and OR, respectively.
But what if you want to use logical AND/OR for raw
arrays, or bit-wise AND/OR for integer
arrays?
When using the overloaded operators, using logical AND/OR for type raw
necessitates converting the vector/array to type logical
, thus making a copy.
With the bc.*()
functions, this is not necessary.
Want to use logical AND/OR? Use bc.b()
, it supports several types including raw
.
Want to use bit-wise AND/OR? Use bc.bit()
, it supports not only raw
, but integer
as well.
7 bc_chain() function vs broadcaster
class
As stated in the introduction, the ‘broadcast’ package provides 2 ways to overload the base operator to use broadcasting.
One way is to use the bc_chain() function. The user can supply a formula with a mathematical expression in bc_chain() - like so: bc_chain(~ x + y / z)
, and the base operators in the expression will use the overloaded broadcasted operators.
The other way is to designate a vector or array as “broadcaster”.
Like so: broadcaster(x) <- TRUE
.
The “broadcast” class has its own method dispatches for the base operators.
So if either side of the base operators is of class “broadcaster”, the broadcasted overloaded operator is used.
Both ways have their own advantages and disadvantages.
The primary disadvantage of bc_chain() is that it has rather large overhead, because a formula needs to be translated to an expression that can evaluated, AND the operators need to be overloaded ‘on-the-fly’. The broadcaster class has no such problem.
The primary disadvantage of the broadcaster class, is that it may have to compete with other classed. For example, if x
is a “broadcaster”, and y
is a “bit64” class, they will compete for the method dispatch. The bc_chain() method has no such problem.
8 Compatibility between ‘broadcaster’ and other classes
When an object is set to be a “broadcaster” using the broadcaster()<-
operation, the ‘broadcaster’ class is before custom classes such as “bit64” (ignoring internal classes like “array”, “matrix”, “numeric”, etc.), to ensure compatibility.
Thus if a custom class provide method dispatches for base operators, like the “bit64” class, the method dispatch for said will (or at least should) win over the “broadcaster” class.
It is up to the authors of such classes to decide whether to incorporate broadcasting in their method dispatches.