com.phasmidsoftware.number.core.Expression.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of number_2.13 Show documentation
Show all versions of number_2.13 Show documentation
Fuzzy, Lazy Scala library for numerical computation
The newest version!
/*
* Copyright (c) 2023. Phasmid Software
*/
package com.phasmidsoftware.number.core
import com.phasmidsoftware.matchers.MatchLogger
import com.phasmidsoftware.number.core.FP.recover
import com.phasmidsoftware.number.parse.ShuntingYardParser
import scala.language.implicitConversions
/**
* Trait Expression which defines the behavior of a lazily-evaluated tree of mathematical operations and operands.
*/
trait Expression extends NumberLike {
/**
* Method to determine if this Expression cannot be simplified on account of it being atomic.
*
* @return true if this extends AtomicExpression
*/
def isAtomic: Boolean
/**
* Method to determine if this Expression is based solely on a particular Factor and, if so, which.
*
* @return Some(factor) if expression only involves that factor; otherwise None.
*/
def maybeFactor: Option[Factor]
/**
* Action to evaluate this Expression as a Field,
* NOTE no simplification occurs here.
* Therefore, if an expression cannot be evaluated exactly,
* then it will result in a fuzzy number.
*
* @return the value.
*/
def evaluate: Field
/**
* Action to materialize this Expression as a Field.
* If possible, this Expression will be simplified first.
*
* @return the materialized Field.
*/
def materialize: Field = Expression.em.simplifyAndEvaluate(this)
/**
* Method to determine if this Expression corresponds to a real Number.
*
* @return a Some(x) if this is a Number; otherwise return None.
*/
def asNumber: Option[Number] = materialize.asNumber
/**
* Method to determine the depth of this Expression.
*
* @return the depth (an atomic expression has depth of 1).
*/
def depth: Int
/**
* Eagerly compare this Expression with comparand.
*
* TODO this will work only for Numbers. We need to be able to determine if two Complex numbers are essentially the same.
*
* @param comparand the expression to be compared.
* @return the result of comparing materialized this with materialized comparand.
*/
def compare(comparand: Expression): Int = recover(for (x <- materialize.asNumber; y <- comparand.materialize.asNumber) yield x.compare(y), NumberException("compare: logic error"))
}
object Expression {
// NOTE this is where we turn logging on (by using LogDebug or LogInfo).
implicit val logger: MatchLogger = MatchLogger(com.phasmidsoftware.matchers.LogOff, classOf[Expression])
implicit val em: ExpressionMatchers = new ExpressionMatchers {}
// trait LoggableExpression extends Loggable[Expression] {
// def toLog(t: Expression): String = t.render
// }
// implicit object LoggableExpression extends LoggableExpression
//
// val flog = Flog[ExpressionMatchers]
/**
* The following method is helpful in getting an expression from a Field.
*
* CONSIDER improving the logic.
*/
def apply(x: Field): Expression = x match {
case Constants.zero => Zero
case Constants.one => One
case Constants.minusOne => MinusOne
case Constants.two => Two
case Constants.pi => ConstPi
case Constants.twoPi => ConstPi * 2
case Constants.e => ConstE
case _ => Literal(x)
}
/**
* The following method is helpful in getting an expression started.
*/
def apply(x: Int): Expression = x match {
case 0 => Zero
case 1 => One
case _ => Expression(Real(x))
}
/**
* Method to parse a String as an Expression.
*
* TODO this may not accurately parse all infix expressions.
* The idea is for render and parse.get to be inverses.
* NOTE that it might be a problem with render instead.
*/
def parse(x: String): Option[Expression] = ShuntingYardParser.parseInfix(x).toOption flatMap (_.evaluate)
/**
* The following constants are helpful in getting an expression started.
*/
val zero: Expression = Zero
val one: Expression = One
val pi: Expression = Expression(Constants.pi)
val e: Expression = Expression(Constants.e)
/**
* Other useful expressions.
*/
val phi: Expression = (one + Constants.root5) / Literal(Number.two)
implicit def convertFieldToExpression(f: Field): Expression = Expression(f)
implicit def convertIntToExpression(x: Int): Expression = Literal(x)
/**
* Implicit class to allow various operations to be performed on an Expression.
*
* @param x an Expression.
*/
implicit class ExpressionOps(x: Expression) {
/**
* Method to lazily multiply x by y.
*
* @param y another Expression.
* @return an Expression which is the lazy product of x and y.
*/
def plus(y: Expression): Expression = BiFunction(x, y, Sum)
/**
* Method to lazily multiply x by y.
*
* @param y another Expression.
* @return an Expression which is the lazy product of x and y.
*/
def +(y: Expression): Expression = x plus y
/**
* Method to lazily subtract the Field y from x.
*
* @param y a Field.
* @return an Expression which is the lazy product of x and y.
*/
def -(y: Expression): Expression = BiFunction(x, -y, Sum)
/**
* Method to lazily change the sign of this expression.
*
* TESTME
*
* @return an Expression which is this negated.
*/
def unary_- : Expression = BiFunction(x, MinusOne, Product)
/**
* Method to lazily multiply x by y.
*
* @param y a Number.
* @return an Expression which is the lazy product of x and y.
*/
def *(y: Expression): Expression = BiFunction(x, y, Product)
/**
* Method to lazily yield the reciprocal of x.
*
* @return an Expression representing the reciprocal of x.
*/
def reciprocal: Expression = BiFunction(x, MinusOne, Power)
/**
* Method to lazily divide x by y.
*
* @param y a Number.
* @return an Expression which is the lazy quotient of x / y.
*/
def /(y: Expression): Expression = *(y.reciprocal)
/**
* Method to lazily raise x to the power of y.
*
* @param y the power to which x should be raised (an Expression).
* @return an Expression representing x to the power of y.
*/
def ^(y: Expression): Expression = BiFunction(x, y, Power)
/**
* Method to lazily get the square root of x.
*
* @return an Expression representing the square root of x.
*/
def sqrt: Expression = this ^ Literal(2).reciprocal
/**
* Method to lazily get the sine of x.
*
* @return an Expression representing the sin(x).
*/
def sin: Expression = Function(x, Sine)
/**
* Method to lazily get the cosine of x.
*
* @return an Expression representing the cos(x).
*/
def cos: Expression = Function(x, Cosine)
/**
* Method to lazily get the tangent of x.
*
* TESTME
*
* @return an Expression representing the tan(x).
*/
def tan: Expression = sin * cos.reciprocal
/**
* Method to lazily get the natural log of x.
*
* @return an Expression representing the log of x.
*/
def log: Expression = Function(x, Log)
/**
* Method to lazily get the value of e raised to the power of x.
*
* @return an Expression representing e raised to the power of x.
*/
def exp: Expression = Function(x, Exp)
/**
* Method to lazily get the value of atan2(x, y), i.e. if the result is z, then tan(z) = y/x.
*
* @return an Expression representing atan2(x, y).
*/
def atan(y: Expression): Expression = BiFunction(x, y, Atan)
/**
* Eagerly compare this expression with y.
*
* @param comparand the number to be compared.
* @return the result of the comparison.
*/
def compare(comparand: Expression): Int = x compare comparand
}
}
/**
* An Expression which cannot be further simplified.
*/
trait AtomicExpression extends Expression {
def isAtomic: Boolean = true
/**
* @return 1.
*/
def depth: Int = 1
override def hashCode(): Int = materialize.hashCode()
override def equals(obj: Any): Boolean = obj match {
case x: AtomicExpression => materialize == x.materialize
case _ => false
}
}
object AtomicExpression {
def unapply(arg: AtomicExpression): Option[Field] = arg match {
case c: Complex => Some(c)
case Literal(x) => Some(x)
case c: Constant => Some(c.evaluate)
case f: Field => Some(f)
// case g: GeneralNumber => Some(g)
case _ => None
}
}
/**
* An abstract class which extends Expression while providing an instance of ExpressionMatchers for use
* with simplification.
*
*/
abstract class CompositeExpression extends Expression {
def isAtomic: Boolean = false
}
/**
* An AtomicExpression which represents a Number.
*
* @param x the Number.
*/
case class Literal(x: Field) extends AtomicExpression {
/**
* Method to determine if this Expression can be evaluated exactly.
*
* @return true if materialize will result in an ExactNumber, else false.
*/
def isExact(maybeFactor: Option[Factor]): Boolean = x.isExact(maybeFactor)
/**
* @return Some(factor).
*/
def maybeFactor: Option[Factor] = x match {
case Real(n) => Some(n.factor)
case c: BaseComplex => if (c.real.factor == c.imag.factor) Some(c.real.factor) else None
}
/**
* Action to evaluate this Expression as a Field,
*
* @return x.
*/
def evaluate: Field = x
/**
* Action to materialize this Expression and render it as a String,
* that is to say we eagerly evaluate this Expression as a String.
*
* @return a String representing the value of this expression.
*/
def render: String = x.toString
/**
* Generate a String for debugging purposes.
*
* @return a String representation of this Literal.
*/
override def toString: String = s"$x"
}
object Literal {
def unapply(arg: Literal): Option[Field] = Some(arg.x)
def apply(x: Int): Literal = Literal(Real(x))
def apply(x: Rational): Literal = Literal(Real(x))
def apply(x: Double): Literal = Literal(Real(x))
def apply(x: Number): Literal = Literal(Real(x))
}
/**
* A known constant value, for example π (pi) or e.
*/
abstract class Constant extends AtomicExpression {
/**
* Method to determine if this Expression can be evaluated exactly.
*
* Important NOTE: Some constants will be fuzzy in which case this method must be overridden.
*
* @return true.
*/
def isExact(maybeFactor: Option[Factor]): Boolean = evaluate.isExact(maybeFactor)
/**
* Action to materialize this Expression and render it as a String,
* that is to say we eagerly evaluate this Expression as a String.
*
* @return String form of this Constant.
*/
def render: String = evaluate.render
/**
* Method to yield a String from this Constant.
*
* @return a String.
*/
override def toString: String = render
}
case object Zero extends Constant {
/**
* @return Number.zero
*/
def evaluate: Field = Constants.zero
def maybeFactor: Option[Factor] = Some(Scalar)
}
case object One extends Constant {
/**
* @return 1.
*/
def evaluate: Field = Constants.one
def maybeFactor: Option[Factor] = Some(Scalar)
}
case object MinusOne extends Constant {
/**
* @return -1.
*/
def evaluate: Field = Constants.minusOne
def maybeFactor: Option[Factor] = Some(Scalar)
/**
* Action to materialize this Expression and render it as a String,
* that is to say we eagerly evaluate this Expression as a String.
*
* @return "1-".
*/
override def render: String = "-1"
}
case object Two extends Constant {
/**
* @return 2.
*/
def evaluate: Field = Constants.two
def maybeFactor: Option[Factor] = Some(Scalar)
}
/**
* The constant π (pi).
* Yes, this is an exact number.
*/
case object ConstPi extends Constant {
/**
* @return pi.
*/
def evaluate: Field = Constants.pi
def maybeFactor: Option[Factor] = Some(Radian)
}
/**
* The constant e.
* Yes, this is an exact number.
*/
case object ConstE extends Constant {
/**
* @return e.
*/
def evaluate: Field = Constants.e
/**
* TESTME
*
* @return Some(factor) if expression only involves that factor; otherwise None.
*/
def maybeFactor: Option[Factor] = Some(NatLog)
}
/**
* This class represents a monadic function of the given expression.
*
* @param x the expression being operated on.
* @param f the function to be applied to x.
*/
case class Function(x: Expression, f: ExpressionFunction) extends CompositeExpression {
/**
* Method to determine if this Expression can be evaluated exactly.
*
* @param maybeFactor the context in which we want to evaluate this Expression.
* @return false.
*/
def isExact(maybeFactor: Option[Factor]): Boolean = f(x.materialize).isExact(maybeFactor)
/**
* TODO implement properly according to the actual function involved.
*
* TESTME
*
* @return Some(factor) if expression only involves that factor; otherwise None.
*/
def maybeFactor: Option[Factor] = None
/**
* Method to determine the depth of this Expression.
*
* TESTME
*
* @return the 1 + depth of x.
*/
def depth: Int = 1 + x.depth
/**
* Action to materialize this Expression as a Field.
*
* @return the materialized Field.
*/
def evaluate: Field = f(x.materialize)
/**
* Action to materialize this Expression and render it as a String.
*
* TESTME.
*
* @return a String representing the value of this expression.
*/
def render: String = materialize.toString
/**
* TESTME
*
* @return
*/
override def toString: String = s"$f($x)"
}
/**
* This class represents a dyadic function of the two given expressions.
*
* @param a the first expression being operated on.
* @param b the second expression being operated on.
* @param f the function to be applied to a and b.
*/
case class BiFunction(a: Expression, b: Expression, f: ExpressionBiFunction) extends CompositeExpression {
/**
* Method to determine if this Expression can be evaluated exactly.
*
* @return the value of exact which is based on a, b, and f.
*/
def isExact(maybeFactor: Option[Factor]): Boolean = exact && value.isExact(maybeFactor)
/**
* Method to determine if this Expression is based solely on a particular Factor and, if so, which.
*
* @return the value of factorsMatch for the function f and the results of invoking maybeFactor on each operand..
*/
def maybeFactor: Option[Factor] = for (f1 <- a.maybeFactor; f2 <- b.maybeFactor; r <- factorsMatch(f, f1, f2)) yield r
/**
* Method to determine the depth of this Expression.
*
* @return the depth (an atomic expression has depth of 1).
*/
def depth: Int = 1 + math.max(a.depth, b.depth)
/**
* Action to materialize this Expression as a Field.
*
* @return the materialized Field.
*/
def evaluate: Field = value
/**
* Action to materialize this Expression and render it as a String.
*
* @return a String representing the value of this expression.
*/
def render: String = materialize.render
/**
* Render this BiFunction for debugging purposes.
*
* @return a String showing a, f, and b in parentheses (or in braces if not exact).
*/
override def toString: String = if (exact) s"($a $f $b)" else s"{$a $f $b}"
// NOTE that NatLog numbers don't behave like other numbers so...
// CONSIDER really should be excluded from all cases
private def factorsMatch(f: ExpressionBiFunction, f1: Factor, f2: Factor): Option[Factor] = f match {
case Sum if f1 == f2 && f1 != NatLog =>
Some(f1)
case Product if f1 == f2 || f1 == Scalar || f2 == Scalar =>
if (f1 == f2) Some(f1) else if (f2 == Scalar) Some(f1) else Some(f2)
case Power if f2 == Scalar =>
Some(f1)
case _ =>
None
}
private lazy val value: Field = f(a.materialize, b.materialize)
/**
* Determine if this dyadic expression has an exact result, according to f, the function.
* NOTE that Product is always true, but it is possible that Sum or Power could be false.
*
* @return true if the result of the f(a,b) is exact.
* NOTE: this appears to be an inaccurate description of the result.
*/
private lazy val conditionallyExact: Boolean = f match {
case Power => b.materialize.asNumber.flatMap(x => x.toInt).isDefined
case Sum => maybeFactor.isDefined
case Product => true
case _ => false
}
/**
* Regular hashCode method.
*
* @return an Int depending on f, a, and b.
*/
override def hashCode(): Int = java.util.Objects.hash(f, a, b)
/**
* An equals method which considers two BiFunctions, which are non-identical but symmetric, to be equal.
*
* @param obj the other object.
* @return true if the values of these two expressions would be the same (without any evaluation).
*/
override def equals(obj: Any): Boolean = obj match {
case BiFunction(c, d, g) => f == g && (a == c && b == d | f != Power && a == d && b == c)
case _ => false
}
private lazy val exact: Boolean = a.isExact(None) && b.isExact(None) && (f.isExact || conditionallyExact)
}
case object Sine extends ExpressionFunction(x => x.sin, "sin")
case object Cosine extends ExpressionFunction(x => x.cos, "cos")
case object Atan extends ExpressionBiFunction((x: Field, y: Field) => (for (a <- x.asNumber; b <- y.asNumber) yield Real(a atan b)).getOrElse(Real(Number.NaN)), "atan", false, false)
case object Log extends ExpressionFunction(x => x.log, "log")
case object Exp extends ExpressionFunction(x => x.exp, "exp")
case object Sum extends ExpressionBiFunction((x, y) => x add y, "+", isExact = false)
case object Product extends ExpressionBiFunction((x, y) => x multiply y, "*", isExact = true)
case object Power extends ExpressionBiFunction((x, y) => x.power(y), "^", isExact = false, commutes = false)
/**
* A lazy monadic expression function.
*
* TODO need to mark whether this function is exact or not (but I can't think of many which are exact).
*
* TODO implement also for other fields than Numbers.
*
* @param f the function Number => Number.
* @param name the name of this function.
*/
class ExpressionFunction(val f: Number => Number, val name: String) extends (Field => Field) {
/**
* Evaluate this function on Field x.
*
* @param x the parameter to the function.
* @return the result of f(x).
*/
override def apply(x: Field): Field =
recover(x.asNumber map f map (Real(_)), ExpressionException(s"logic error: ExpressionFunction.apply($x)"))
/**
* Generate helpful debugging information about this ExpressionFunction.
*
* @return a String.
*/
override def toString: String = s"$name"
}
object ExpressionFunction {
def unapply(arg: ExpressionFunction): Option[(Number => Number, String)] = Some(arg.f, arg.name)
}
/**
* A lazy dyadic expression function.
*
* @param f the function (Field, Field) => Field
* @param name the name of this function.
* @param isExact true if this function is always exact, given exact inputs.
* @param commutes true only if the parameters to f are commutative.
*/
class ExpressionBiFunction(val f: (Field, Field) => Field, val name: String, val isExact: Boolean, val commutes: Boolean = true) extends ((Field, Field) => Field) {
/**
* Evaluate this function on x.
*
* @param a the first parameter to the function.
* @param b the second parameter to the function.
* @return the result of f(x).
*/
override def apply(a: Field, b: Field): Field = f(a, b)
/**
* Generate helpful debugging information about this ExpressionFunction.
*
* @return a String.
*/
override def toString: String = s"$name"
}
object ExpressionBiFunction {
def unapply(f: ((Field, Field)) => Field): Option[((Field, Field) => Field, String)] = f match {
case e: ExpressionBiFunction => Some(e.f, e.name)
case _ => None
}
}
case class ExpressionException(str: String) extends Exception(str)