symtegration-0.6.1: src/Symtegration/Symbolic.hs
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE PatternSynonyms #-}
-- |
-- Module: Symtegration.Symbolic
-- Description: Library for symbolically representing mathematical expressions.
-- Copyright: Copyright 2024 Yoo Chung
-- License: Apache-2.0
-- Maintainer: dev@chungyc.org
module Symtegration.Symbolic
( -- * Representation
Expression (..),
UnaryFunction (..),
BinaryFunction (..),
-- * Manipulation
substitute,
-- * Computation
evaluate,
fractionalEvaluate,
toFunction,
getUnaryFunction,
getBinaryFunction,
-- * Pattern synonyms
-- | Pattern synonyms are defined to make it more convenient to pattern match on 'Expression'.
-- ** Constants
pattern Pi',
-- ** Unary functions
pattern Negate',
pattern Abs',
pattern Signum',
pattern Exp',
pattern Log',
pattern Sqrt',
pattern Sin',
pattern Cos',
pattern Tan',
pattern Asin',
pattern Acos',
pattern Atan',
pattern Sinh',
pattern Cosh',
pattern Tanh',
pattern Asinh',
pattern Acosh',
pattern Atanh',
-- ** Binary functions
pattern (:+:),
pattern (:*:),
pattern (:-:),
pattern (:/:),
pattern (:**:),
pattern LogBase',
)
where
import Data.Ratio
import Data.String (IsString, fromString)
import Data.Text
import GHC.Generics (Generic)
import TextShow (TextShow)
import TextShow.Generic (FromGeneric (..))
-- $setup
-- >>> import Symtegration
-- | Symbolic representation of a mathematical expression.
-- It is an instance of the 'Num', 'Fractional', and 'Floating' type classes,
-- so normal Haskell expressions can be used, although the expressions
-- are limited to using the functions defined by these type classses.
-- The type is also an instance of the 'IsString' type class,
-- so symbols can be expressed as Haskell string with the @OverloadedStrings@ extension.
-- The structure of these values is intended to be visible.
--
-- >>> 2 :: Expression
-- Number 2
-- >>> "x" :: Expression
-- Symbol "x"
-- >>> 2 + sin "x" :: Expression
-- BinaryApply Add (Number 2) (UnaryApply Sin (Symbol "x"))
--
-- A somewhat more concise representation can be obtained using 'Symtegration.toHaskell':
--
-- >>> toHaskell $ 2 * "y" + sin "x"
-- "2 * y + sin x"
data Expression
= -- | Represents a concrete number.
Number Integer
| -- | Represents a symbol, which could either be a variable or a constant.
Symbol Text
| -- | Represents the application of an unary function.
UnaryApply UnaryFunction Expression
| -- | Represents the application of a binary function.
BinaryApply BinaryFunction Expression Expression
deriving
( -- | Structural equality, not semantic equality.
-- E.g., @"a" - "a" /= 0@.
Eq,
Show,
Read,
Generic
)
deriving (TextShow) via FromGeneric Expression
pattern Pi' :: Expression
pattern Pi' = Symbol "pi"
-- | Symbolic representation for unary functions.
data UnaryFunction
= -- | 'negate'
Negate
| -- | 'abs'
Abs
| -- | 'signum'
Signum
| -- | 'exp'
Exp
| -- | 'log'
Log
| -- | 'sqrt'
Sqrt
| -- | 'sin'
Sin
| -- | 'cos'
Cos
| -- | 'tan'
Tan
| -- | 'asin'
Asin
| -- | 'acos'
Acos
| -- | 'atan'
Atan
| -- | 'sinh'
Sinh
| -- | 'cosh'
Cosh
| -- | 'tanh'
Tanh
| -- | 'asinh'
Asinh
| -- | 'acosh'
Acosh
| -- | 'atanh'
Atanh
deriving (Eq, Enum, Bounded, Show, Read, Generic)
deriving (TextShow) via FromGeneric UnaryFunction
pattern Negate', Abs', Signum', Exp', Log', Sqrt', Sin', Cos', Tan', Asin', Acos', Atan', Sinh', Cosh', Tanh', Asinh', Acosh', Atanh' :: Expression -> Expression
pattern Negate' x = UnaryApply Negate x
pattern Abs' x = UnaryApply Abs x
pattern Signum' x = UnaryApply Signum x
pattern Exp' x = UnaryApply Exp x
pattern Log' x = UnaryApply Log x
pattern Sqrt' x = UnaryApply Sqrt x
pattern Sin' x = UnaryApply Sin x
pattern Cos' x = UnaryApply Cos x
pattern Tan' x = UnaryApply Tan x
pattern Asin' x = UnaryApply Asin x
pattern Acos' x = UnaryApply Acos x
pattern Atan' x = UnaryApply Atan x
pattern Sinh' x = UnaryApply Sinh x
pattern Cosh' x = UnaryApply Cosh x
pattern Tanh' x = UnaryApply Tanh x
pattern Asinh' x = UnaryApply Asinh x
pattern Acosh' x = UnaryApply Acosh x
pattern Atanh' x = UnaryApply Atanh x
-- | Symbolic representation for binary functions.
data BinaryFunction
= -- | '(+)'
Add
| -- | '(*)'
Multiply
| -- | '(-)'
Subtract
| -- | '(/)'
Divide
| -- | '(**)'
Power
| -- | 'logBase'
LogBase
deriving (Eq, Enum, Bounded, Show, Read, Generic)
deriving (TextShow) via FromGeneric BinaryFunction
pattern (:+:), (:*:), (:-:), (:/:), (:**:), LogBase' :: Expression -> Expression -> Expression
pattern x :+: y = BinaryApply Add x y
pattern x :*: y = BinaryApply Multiply x y
pattern x :-: y = BinaryApply Subtract x y
pattern x :/: y = BinaryApply Divide x y
pattern x :**: y = BinaryApply Power x y
pattern LogBase' x y = BinaryApply LogBase x y
instance IsString Expression where
fromString = Symbol . fromString
instance Num Expression where
(+) = BinaryApply Add
(-) = BinaryApply Subtract
(*) = BinaryApply Multiply
negate = UnaryApply Negate
abs = UnaryApply Abs
signum = UnaryApply Signum
fromInteger = Number
instance Fractional Expression where
(/) = BinaryApply Divide
fromRational q | d == 1 = n | otherwise = BinaryApply Divide n d
where
n = Number $ numerator q
d = Number $ denominator q
instance Floating Expression where
pi = Symbol "pi"
exp = UnaryApply Exp
log = UnaryApply Log
sqrt = UnaryApply Sqrt
(**) = BinaryApply Power
logBase = BinaryApply LogBase
sin = UnaryApply Sin
cos = UnaryApply Cos
tan = UnaryApply Tan
asin = UnaryApply Asin
acos = UnaryApply Acos
atan = UnaryApply Atan
sinh = UnaryApply Sinh
cosh = UnaryApply Cosh
tanh = UnaryApply Tanh
asinh = UnaryApply Asinh
acosh = UnaryApply Acosh
atanh = UnaryApply Atanh
-- | Returns a function corresponding to the symbolic representation of an unary function.
--
-- >>> (getUnaryFunction Cos) pi == (cos pi :: Double)
-- True
getUnaryFunction :: (Floating a) => UnaryFunction -> (a -> a)
getUnaryFunction Negate = negate
getUnaryFunction Abs = abs
getUnaryFunction Signum = signum
getUnaryFunction Exp = exp
getUnaryFunction Log = log
getUnaryFunction Sqrt = sqrt
getUnaryFunction Sin = sin
getUnaryFunction Cos = cos
getUnaryFunction Tan = tan
getUnaryFunction Asin = asin
getUnaryFunction Acos = acos
getUnaryFunction Atan = atan
getUnaryFunction Sinh = sinh
getUnaryFunction Cosh = cosh
getUnaryFunction Tanh = tanh
getUnaryFunction Asinh = asinh
getUnaryFunction Acosh = acosh
getUnaryFunction Atanh = atanh
-- | Returns a function corresponding to the symbolic representation of a binary function.
--
-- >>> (getBinaryFunction Add) 2 5 == (2 + 5 :: Double)
-- True
getBinaryFunction :: (Floating a) => BinaryFunction -> (a -> a -> a)
getBinaryFunction Add = (+)
getBinaryFunction Multiply = (*)
getBinaryFunction Subtract = (-)
getBinaryFunction Divide = (/)
getBinaryFunction Power = (**)
getBinaryFunction LogBase = logBase
-- | Substitute the symbols with the corresponding expressions they are mapped to.
-- The symbols will be replaced as is; there is no special treatment if the
-- expression they are replaced by also contains the same symbol.
--
-- >>> toHaskell $ substitute ("x" + "y") (\case "x" -> Just ("a" * "b"); "y" -> Just 4)
-- "a * b + 4"
substitute ::
-- | Expression to apply substitution.
Expression ->
-- | Maps symbols to expressions they are to be substituted with.
(Text -> Maybe Expression) ->
-- | Expression with substitution applied.
Expression
substitute e@(Number _) _ = e
substitute e@(Symbol s) f
| (Just x) <- f s = x
| otherwise = e
substitute (UnaryApply func x) f = UnaryApply func (substitute x f)
substitute (BinaryApply func x y) f = BinaryApply func (substitute x f) (substitute y f)
-- | Calculates the value for a mathematical expression for a given assignment of values to symbols.
--
-- For example, when \(x=5\), then \(2x+1=11\).
--
-- >>> evaluate (2 * "x" + 1) (\case "x" -> Just 5)
-- Just 11.0
--
-- All symbols except for @"pi"@ in a mathematical expression must be assigned a value.
-- Otherwise, a value cannot be computed.
--
-- >>> evaluate (2 * "x" + 1) (const Nothing)
-- Nothing
--
-- The symbol @"pi"@ is always used to represent \(\pi\),
-- and any assignment to @"pi"@ will be ignored.
-- For example, the following is \(\pi - \pi\), not \(100 - \pi\).
--
-- >>> evaluate ("pi" - pi) (\case "x" -> Just 100)
-- Just 0.0
evaluate ::
(Floating a) =>
-- | Mathematical expression to evaluate.
Expression ->
-- | Maps symbols to concrete values.
(Text -> Maybe a) ->
-- | Evaluation result.
Maybe a
evaluate (Number n) _ = Just $ fromInteger n
evaluate (Symbol "pi") _ = Just pi
evaluate (Symbol x) m = m x
evaluate (UnaryApply fun expr) m = fmap f v
where
f = getUnaryFunction fun
v = evaluate expr m
evaluate (BinaryApply fun expr1 expr2) m = f <$> v1 <*> v2
where
f = getBinaryFunction fun
v1 = evaluate expr1 m
v2 = evaluate expr2 m
-- |
-- Evaluates a mathematical expression with only operations available to 'Fractional' values.
-- In particular, this allows exact evaluations with 'Rational' values.
-- 'Nothing' will be returned if a function not supported by all 'Fractional' values
-- is used by the mathematical expression.
--
-- As an exception, the '(**)' operator is allowed with constant integer exponents,
-- even though '(**)' is not a function applicable to all 'Fractional' types.
--
-- For example,
--
-- >>> let p = 1 / (3 * "x"**5 - 2 * "x" + 1) :: Expression
-- >>> fractionalEvaluate p (\case "x" -> Just (2 / 7 :: Rational))
-- Just (16807 % 7299)
--
-- Compare against 'evaluate', which cannot even use 'Rational' computations
-- because 'Rational' is not an instance of the 'Floating' type class:
--
-- >>> evaluate p (\case "x" -> Just (2 / 7 :: Double))
-- Just 2.3026441978353196
fractionalEvaluate ::
(Eq a, Fractional a) =>
-- | Mathematical expression to evaluate.
Expression ->
-- | Maps symbols to concrete values.
(Text -> Maybe a) ->
-- | Evaluation result.
Maybe a
fractionalEvaluate (Number n) _ = Just $ fromInteger n
fractionalEvaluate (Symbol x) m = m x
fractionalEvaluate (Negate' x) m = negate <$> fractionalEvaluate x m
fractionalEvaluate (Abs' x) m = abs <$> fractionalEvaluate x m
fractionalEvaluate (Signum' x) m = signum <$> fractionalEvaluate x m
fractionalEvaluate (x :+: y) m = (+) <$> fractionalEvaluate x m <*> fractionalEvaluate y m
fractionalEvaluate (x :-: y) m = (-) <$> fractionalEvaluate x m <*> fractionalEvaluate y m
fractionalEvaluate (x :*: y) m = (*) <$> fractionalEvaluate x m <*> fractionalEvaluate y m
fractionalEvaluate (x :/: y) m
| Just 0 <- y' = Nothing
| otherwise = (/) <$> x' <*> y'
where
x' = fractionalEvaluate x m
y' = fractionalEvaluate y m
fractionalEvaluate (x :**: (Number n)) m = (^^ n) <$> fractionalEvaluate x m
fractionalEvaluate _ _ = Nothing
-- | Returns a function based on a given expression. This requires
-- a specification of how a symbol maps the argument to a value
-- to be used in its place.
--
-- For example, the symbol "x" could use the argument as is as its value.
-- I.e., "x" can be mapped to a function which maps the argument to itself.
--
-- >>> let f = toFunction ("x" ** 2 + 1) (\case "x" -> id) :: Double -> Double
-- >>> f 3 -- 3 ** 2 + 1
-- 10.0
-- >>> f 10 -- 10 ** 2 + 1
-- 101.0
--
-- For another example, "x" could map the first element from a tuple argument,
-- and "y" could map the second element from the tuple argument. I.e.,
-- for a tuple argument to the function, the first element will be used as "x"
-- and the second element will be used as "y".
--
-- >>> let m = \case "x" -> (\(x,_) -> x); "y" -> (\(_,y) -> y)
-- >>> let g = toFunction ("x" + 2 * "y") m :: (Double, Double) -> Double
-- >>> g (3,4) -- 3 + 2 * 4
-- 11.0
-- >>> g (7,1) -- 7 + 2 * 1
-- 9.0
toFunction ::
(Floating b) =>
-- | The expression to be converted into a function.
Expression ->
-- | Maps how the argument to the function should be mapped to a value for a symbol.
-- E.g., "x" could map the first element in a tuple as the value to use in its place.
(Text -> (a -> b)) ->
-- | The function generated from the expression.
(a -> b)
toFunction (Number n) _ = const $ fromInteger n
toFunction (Symbol s) m = m s
toFunction (UnaryApply func x) m = f . g
where
f = getUnaryFunction func
g = toFunction x m
toFunction (BinaryApply func x y) m = \v -> f (g v) (h v)
where
f = getBinaryFunction func
g = toFunction x m
h = toFunction y m