How does FunctionDomain work?
Evaluate ((x - 2) (x + 1))/((x - 2) (x + 3))
and see what it gives. It automatically simplifies to (1 + x)/(3 + x)
. The second input you show is effectively
FunctionDomain[(1 + x)/(3 + x), x]
The different results you get are not due to FunctionDomain
, but due to the different inputs which are passed to it.
You may wonder if the fully automatic simplification of this fraction should be considered incorrect. There is a closely related discussion here, where I argued that such simplifications are in fact more useful than harmful and quite reasonable:
- A one line proof that one is zero using Mathematica 10
Update
Per @ChipHurst's comment, wrapping the argument in Hold
works too:
FunctionDomain[Hold[((x - 2) (x + 1))/((x - 2) (x + 3))], x]
(* x < -3 || -3 < x < 2 || x > 2 *)
This appears to be an undocumented extension of FunctionDomain
. Only Hold
works, not other function with the HoldAll
attribute (not even HoldForm
).
In Mathematica trivial removable singularities of like x/x
(in full form Times[x, Power[x, -1]]
) are replaced by 1
during ordinary evaluation of Times
, when appropriate pair of positive and negative Power
of same expression is encountered. Similarly 1/(1/x)
is replaced by x
during evaluation of Power
.
To prevent both kinds of replacements, during evaluation, we can dynamically assign dummy head to Power[...]
expressions, using Block
. Then we can wrap resulting expression with Hold
, replace dummy head back with Power
, and pass it to FunctionDomain
:
f // ClearAll
f[x_] := 1/x
Block[{Power = power}, x f[x]] (* x power[x, -1] *)
Hold@Evaluate@% /. power -> Power (* Hold[x/x] *)
FunctionDomain[%, x] (* x<0 || x>0 *)
This technique evaluates expression, just without removing singularities. In contrast simple passing of expression to FunctionDomain
either evaluates expression completely, removing singularities:
FunctionDomain[x f[x], x] (* True *)
or completely prevents evaluation, including evaluation of functions we would like to evaluate:
FunctionDomain[Hold[x f[x]], x]
(* ... FunctionDomain:Unable to find the domain with the available methods. *)
(* FunctionDomain[Hold[x f[x]],x] *)
Package
Function automating above Block
ed evaluation is implemented in following package:
BeginPackage@"Domains`"; Unprotect@"`*"; ClearAll@"`*"
StrictFunctionDomain::usage = "\
StrictFunctionDomain[expr, vars, dom] \
finds the largest domain of definition of expression expr, treated as function \
in given variables vars, with arguments and values in domain dom, \
excludes all singularities encountered while evaluating expr, \
even if those singularities would be removed \
during ordinary evaluation of expr. \
vars can be a symbol or list of symbols. Domain dom can be Reals or Complexes.\
StrictFunctionDomain[expr, vars] \
uses Reals as domain.\
StrictFunctionDomain[expr] or StrictFunctionDomain[expr, Automatic, ...] \
uses variables extracted from expr.";
RestrictDomain::usage = "\
RestrictDomain[expr, vars, dom] \
returns expr with sub-expressions subExpr, for which, \
domain can be restricted, replaced with ConditionalExpression[subExpr, cond] \
where cond are conditions restricting domain of subExpr, \
treated as function in given variables vars, \
with arguments and values in domain dom. \
vars can be a symbol or list of symbols. Domain dom can be Reals or Complexes.\
RestrictDomain[expr, vars] \
uses Reals as domain.\
RestrictDomain[expr] or RestrictDomain[expr, Automatic, ...] \
for each replaced sub-expression uses variables extracted from it.";
Begin@"`Private`"; ClearAll@"`*"
StrictFunctionDomain // Attributes = HoldFirst;
StrictFunctionDomain[
expr_, vars_ : Automatic, dom : Reals | Complexes : Reals,
opts : OptionsPattern@FunctionDomain
] :=
With[{eval = evaluateKeepingSingularities@expr},
FunctionDomain @@ Unevaluated /@ Join[
eval,
getHeldVars[vars]@eval,
HoldComplete[dom, opts]
]
]
RestrictDomain // Attributes = HoldFirst;
RestrictDomain[
expr_, vars_ : Automatic, dom : Reals | Complexes : Reals,
opts : OptionsPattern@FunctionDomain
] :=
ReleaseHold@With[{getHeldVars = getHeldVars@vars},
evaluateKeepingSingularities@expr /.
subExpr : (acceptableHeads@dom)[___] :>
ConditionalExpression[subExpr,
FunctionDomain @@ Unevaluated /@ Join[
HoldComplete@subExpr,
getHeldVars@subExpr,
HoldComplete[dom, opts]
]
]
]
evaluateKeepingSingularities = Function[,
Internal`InheritedBlock[{Power},
With[{protected = Unprotect@Power},
Power@args__ /; Not@VectorQ[{args}, NumericQ] := power@args;
Protect@protected
];
HoldComplete@#&@# /. power -> Power
],
HoldAllComplete
];
getHeldVars // Attributes = HoldAllComplete;
getHeldVars@Automatic = Function[,
(HoldComplete[#]&@Union@Cases[Unevaluated@#,
s_Symbol /; Not@MemberQ[Attributes@s, Constant] :> HoldComplete@s,
{-1}
])[[All, All, 1]],
HoldAllComplete
];
getHeldVars@vars_ := Function[, HoldComplete@vars, HoldAllComplete]
acceptableHeads@Complexes =
Abs | AiryAi | AiryAiPrime | AiryBi | AiryBiPrime | AngerJ | ArcCos |
ArcCosh | ArcCot | ArcCoth | ArcCsc | ArcCsch | ArcSec | ArcSech |
ArcSin | ArcSinh | ArcTan | ArcTanh | Arg | BesselI | BesselJ | BesselK |
BesselY | Beta | Binomial | Boole | Ceiling | ConditionalExpression |
Conjugate | Cos | Cosh | CoshIntegral | CosIntegral | Cot | Coth | Csc |
Csch | CubeRoot | DawsonF | DiscreteDelta | Erf | Erfc | Erfi | Exp |
ExpIntegralE | ExpIntegralEi | Factorial | Factorial2 | Fibonacci | Floor |
FractionalPart | FresnelC | FresnelS | Function | Gamma | GammaRegularized |
GegenbauerC | Gudermannian | HarmonicNumber | Haversine | HermiteH |
Hypergeometric0F1 | Hypergeometric0F1Regularized | Hypergeometric1F1 |
Hypergeometric1F1Regularized | Im | IntegerPart | Integrate |
KroneckerDelta | LambertW | List | Log | Log10 | Log2 | LogGamma |
LogIntegral | Max | Min | Mod | Norm | Plus | Pochhammer | PolyGamma |
Power | PrimePi | ProductLog | Quotient | Re | RiemannSiegelTheta |
RiemannSiegelZ | Round | SawtoothWave | Sec | Sech | Sign | Sin | Sinc |
Sinh | SinhIntegral | SinIntegral | Sqrt | SquareWave | Surd | Tan | Tanh |
Times | TriangleWave | UnitStep | WeberE | Zeta;
acceptableHeads@Reals = Union[acceptableHeads@Complexes,
BarnesG | EllipticE | EllipticK | EllipticNomeQ | InverseEllipticNomeQ |
InverseErf | InverseErfc | InverseGammaRegularized
];
End[]; Protect@"`*"; EndPackage[];
Basic Examples
It works with examples from OP:
StrictFunctionDomain[(x^2 - x - 2)/(x^2 + x - 6), x]
StrictFunctionDomain[((x - 2) (x + 1))/((x - 2) (x + 3)), x]
(* x < -3 || -3 < x < 2 || x > 2 *)
(* x < -3 || -3 < x < 2 || x > 2 *)
In contrast to using simple FunctionDomain
with held argument, StrictFunctionDomain
works no matter when singularities appear during evaluation of expression. Singularities can be present in (nested) function definitions:
ClearAll[f, g]
f[x_] := 1/x
g[x_, y_] := (x - y)/(x - x y f[x])
and they will still be found by StrictFunctionDomain
, even if function by itself evaluates to singularity free expression:
g[x, y] (* 1 *)
StrictFunctionDomain[g[x, y], {x, y}] (* x != 0 && x - y != 0 *)
It'll work also for function Composition
from previous OP's question:
(f@*f)[x] (* x *)
StrictFunctionDomain[(f@*f)[x], x] (* x < 0 || x > 0 *)
Possible Issues
Using dynamic scoping (Block
-like constructs) to change behavior of, as fundamental function as, Power
is not entirely safe. It works as long as Power
is used only in "mathematical expressions". But if a function uses Power
in some procedure, not as "symbolic mathematical expressions" that is returned by this function, then changing behavior of Power
can lead to unexpected behavior of this function.
That's why in above package Internal`InheritedBlock
is used and only behavior of Power
with non-numeric arguments is changed, which is safer, but still not completely safe.