How does FunctionDomain work?

FunctionDomain[(x^2-x-2)/(x^2+x-6),x]

gives

x < -3 || -3 < x < 2 || x > 2.

However, when I factor the numerator and denominator the result is different:

FunctionDomain[((x - 2) (x + 1))/((x - 2) (x + 3)),x]

x < -3 || x > -3

As I understand things, Mathematica first of all simplifies the argument and then applies the function. But both expressions simplify to (x + 1)/(x + 3).

Why is the output different?

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:

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).

• FWIW adding Hold seems to work In[1]:= FunctionDomain[Hold[((x - 2) (x + 1))/((x - 2) (x + 3))],x] Out[1]= x<-3||-3<x<2||x>2 – Chip Hurst Feb 5 '17 at 15:35
• @ChipHurst That was completely unexpected to me. So this is an undocumented extension then? – Szabolcs Feb 5 '17 at 15:37
• @JackLaVigne No, Simplify is not applied. ((x - 2) (x + 1))/((x - 2) (x + 3)) evaluates directly to (1 + x)/(3 + x), without Simplify. FunctionExpand never even sees the expression the OP entered, only the expression that it evaluated to. – Szabolcs Feb 5 '17 at 16:20
• @Szabolcs Got it. The expression fed to FunctionDomain follows the normal evaluation procedure in Mathematica. Thank you for clarifying. – Jack LaVigne Feb 5 '17 at 16:26
• FunctionDomain[Hold[expr_]] is the intended use - primarily for use in WolframAlpha. If you type function domain x/x into WolframAlpha, for example, you'll get $x\neq0$. – Mark McClure Mar 7 '17 at 0:27

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 Blocked 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 /.
ConditionalExpression[subExpr,
FunctionDomain @@ Unevaluated /@ Join[
HoldComplete@subExpr,
getHeldVars@subExpr,
HoldComplete[dom, opts]
]
]
]

evaluateKeepingSingularities = Function[,
InternalInheritedBlock[{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]

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;

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 InternalInheritedBlock is used and only behavior of Power` with non-numeric arguments is changed, which is safer, but still not completely safe.