hascal-1.4.2: Hascal.hs
-- |Hascal is both a simple but extendable calculator library for Haskell
-- and a command-line program using it.
--
-- Also, its source code is a nice example for a minimalistic Haskell project.
--
-- Some examples for the usage of the command-line program (using bash):
--
-- >>> hascal 1+2
-- 3.0
--
-- >>> hascal 1+2*3-4/198^2
-- 6.99989796959493929190898887868584838281807978777676
--
-- Also, preceding exclamation marks mean that the following number is
-- imaginary, that is, you have to multiply it with i. E.g.:
--
-- >>> hascal _1 ^ 0.5
-- !1.0
--
-- And as you can see, negative numbers are preceded by an underscore.
--
-- Although hascal itself doesn't understand brackets, you can use your shell
-- to get that functionality, like this (using bash):
--
-- >>> hascal e ^ $(hascal i*pi)
-- -1.0
--
-- Speaking of shells, you should consider that your shell might extend an
-- asterisk (*) to the files at the current directory, like this:
--
-- >>> echo *
-- _darcs dist hascal.cabal Hascal.hs LICENSE Main.hs README.org Setup.hs
--
-- That's why this might not work:
--
-- >>> hascal 1 * 2
-- Error. :(
--
-- But you could do this instead:
--
-- >>> hascal 1*2
-- 2
--
-- Or, you could do:
--
-- >>> hascal '1*2'
-- 2
--
-- Yeah, that's pretty much it. Hascal is really minimalistic.
-- And I'm not planning to extend it much.
module Hascal (
-- * Operators
operators,
-- * Evaluators
eval,
hascal,
-- * Pretty Printers
prettyPrint
) where
import Control.Arrow (second)
import Data.Complex (Complex(..))
import Data.Functor ((<$>))
import Data.List (find)
-- |'operators' is the default list of operators.
--
-- An operator consists of one character and a function.
--
-- 'operators' includes:
--
-- * addition, represented by @\'+\'@,
--
-- * subtraction, represented by @\'-\'@,
--
-- * multiplication, represented by @\'*\'@,
--
-- * division, represented by @\'\/\'@,
--
-- * exponentiation, represented by @\'^\'@, and
--
-- * logarithming (with flipped arguments, see below), represented by @\'?\'@,
--
-- such that these laws are held:
--
-- > (a - b == c) == (a == b + c)
-- > (a / b == c) == (a == b * c)
-- > (a ? b == c) == (a == b ^ c)
operators :: RealFloat t
=> [(Char, Complex t -> Complex t -> Complex t)]
operators = [ ('+', (+))
, ('-', (-))
, ('/', (/))
, ('*', (*))
, ('^', (**))
, ('?', flip logBase)
]
calc :: (Read t, RealFloat t)
=> [(Char, Complex t -> Complex t -> Complex t)]
-> String
-> Either String (Complex t)
calc [] a
= readNumber a
calc l@((c,f):s) a
| z /= ""
= case (calc l y,calc l z) of
(Right n,Right m) -> Right (f m n)
(Left n,_ ) -> Left n
(_ ,Left m) -> Left m
| otherwise
= calc s a
where
(y,z) = second (drop 1) $ break (==c) a
-- |'eval' gets a list of operators and a string containing a mathematical
-- expression/term which only uses those operators listed in the first
-- argument, and returns the result of that term.
eval :: (Read t, RealFloat t)
=> [(Char, Complex t -> Complex t -> Complex t)] -- ^ list of operators
-> String -- ^ string containing term
-> Either String (Complex t) -- ^ just result, or nothing
eval = (. reverse) . calc
-- Respects preceding exclamation marks and underscores before a number
readNumber :: (Read t, RealFloat t) => String -> Either String (Complex t)
readNumber x = case reverse x of
('!':s) -> ((0:+1)*) <$> findOrRead s
('_':s) -> negate <$> findOrRead s
('-':s) -> Left ("Error at \"-" ++
s ++
"\".\n\nTo denote negative numbers, " ++
"use a preceding underscore instead.")
s -> findOrRead s
-- Checks whether the string is readable as a mathematical constant before
-- trying to read it as a number
findOrRead :: (Read t, Floating t) => String -> Either String (Complex t)
findOrRead s = maybe (maybeRead s) (Right . snd) $ find ((==s) . fst)
[("pi",pi :+0)
,("e" ,exp 1:+0)
,("i" ,0 :+1)
]
-- Reads numbers
maybeRead :: (Read t, Num t) => String -> Either String (Complex t)
maybeRead s
| any (null . snd) (reads s :: [(Double,String)])
= Right (read s:+0)
| otherwise
= Left ("Error at \"" ++ s ++ "\".")
-- |'hascal' is the default evaluator:
--
-- @ hascal = 'eval' 'operators' @
hascal :: (Read t, RealFloat t) => String -> Either String (Complex t)
hascal = eval operators
-- |'prettyPrint' prints a number nicely.
-- E.g., it doesn't show the real or imaginary part of the number if it's @0@.
prettyPrint :: (Show t, RealFloat t) => Complex t -> String
prettyPrint (r:+0) = show r
prettyPrint (0:+i) = show i ++ "*i"
prettyPrint (r:+i) = show r ++ " + " ++ show i ++ "*i"