2
\$\begingroup\$

I'm writing a program to differentiate functions (perform basic calculus) in Haskell. The program works, but there's one function that looks very ugly to me:

--some prerequisite definitions


--a mathematical value is either...
data Value =
    Literal Double                     --a number, such as 4, -17.5, or 2.33e27 
    | Variable Symbol (Maybe Double)   --a variable, which may or may not have a known value
    | FCall                            --a function call, consisting of a function and two arguments
      {
          func :: Function,
          arg1 :: Value,
          arg2 :: Value
      }
      deriving (Eq)


--simplifies a mathematical value into a simpler form.

reduce :: Value -> Value
reduce val@(Literal _)          = val               --literals cannot be simplified
reduce (Variable _ (Just val))  = Literal val       --bound variables can be simplified to literals
reduce var@(Variable _ Nothing) = var               --unbound variables cannot be simplified
reduce fc@(FCall f a b)         =                   --simplifying functions is messy...
   if isDefined f ma mb
        then matchLiterals ma mb
        else retval
    where ra = reduce a  --reduce simplifies a Value to a Value
          rb = reduce b
          ma = eval ra   --eval converts known values to Doubles and unknown Values to Nothing
          mb = eval rb
          retval = FCall f ra rb  --returned in case of failure
          matchLiterals (Just da) (Just db) = Literal $ (impl f) da db
          matchLiterals _ _ = matchCase $ findCase (cases f) ra rb
          matchCase (Just sc) = evalCase sc ra rb
          matchCase Nothing = retval

The multitude of local functions defined in the where clause seems excessive, but I can't find a better way to handle the flow of execution. I've tried to break this up into several smaller functions, but since most of these can fail (evaluate to Nothing), I still have to use pattern matching to handle the results of these functions.

The flow of logic goes like this:

if the function is defined for the given arguments:
    if the arguments have definite known values:
        evaluate and return the function call
    else:
        look for special cases (adding 0, multiplying/diving by 1, etc.)
        if a special case was found:
            evaluate and return the result of applying the special case to the arguments
        else:
            return another FCall with possibly simplified arguments

else:
    return another FCall with possibly simplified arguments

It's possible I'm fretting over nothing (hah) and this code is perfectly fine, but it seems that this is not the optimal way to handle these conditions. Can anyone offer any advice about how I might better structure this code?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

It is a matter of taste but I think reduce will be a bit more cleaner with case.

reduce exp = case exp of
  Literal{}             -> exp
  Variable _ (Just var) -> Literal var
  Variable _ Nothing    -> exp
  FCall f a b
    | isDefined f ma mb -> matchLiterals ma mb
    | otherwise         -> FCall f ra rb
    where ra = reduce a
          rb = reduce b
          ma = eval ra
          mb = eval rb

Literal{} is special pattern syntax that will still work even if you add more arguments to Literal.

To reduce duplication (just a bit) in where, you can use on function from Data.Function

on :: (b -> b -> c) -> (a -> b) -> a -> a -> c

it is like (.) but allows to apply function to both arguments

(ra,rb) = on (,) reduce a b
(ma,mb) = on (,) eval ra rb
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Also, on is often used infix, e.g f `on` g rather than on f g \$\endgroup\$ Commented May 2, 2014 at 21:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.