"do"ing it right

I feel like there is a better way to do this, but I"m not clear where to start. Currently, I have this code (snippet):

sub :: OpResult -> OpResult -> OpResult
sub (OpInt x) (OpInt y) = OpInt $ x - y
sub (OpInt x) (OpNumber y) = OpNumber $ (toNumber x) - y
sub (OpNumber x) (OpInt y) = OpNumber $ x - (toNumber y)
sub (OpNumber x) (OpNumber y) = OpNumber $ x - y

doTheSubtraction :: Either Error OpResult -> Either Error OpResult -> Either Error OpResult
doTheSubtraction (Left e1) (Left e2) = Left (concat e1 e2)
doTheSubtraction (Left e1) _ = Left e1
doTheSubtraction _ (Left e2) = Left e2
doTheSubtraction (Right x) (Right y) = Right (sub x y)

subtract :: SubtractOperation -> Maybe OpResult -> Effect (Either Error OpResult)
subtract (SubtractOperation r) =
  ( \v -> do
      m <- makeOperate r.minuend v
      s <- makeOperate r.subtrahend v
      pure $ doTheSubtraction m s
  )

makeOperate :: Operation -> Maybe OpResult -> Effect (Either Error OpResult)
makeOperate (FromFormFieldOp op) = getFromFormField op
makeOperate (SubtractOp op) = subtract op
-- ...

There are more Operations. The From return a value in an Effect (Either). The other Ops (subtract) operate on those values. These can be nested.

As you can see I’ve made three separate functions to get the values out of the nested operations and then perform subtraction on them. But is there a way to do all this in the do of subtract. More importantly, is it a better way? What do the experts recommend?

Hi! Since this is only part of the code, it’s hard to give comprehensive feedback. However, I think there are still some useful things to say

  • sub looks reasonable to me. It could be simplified if it always returned an OpNumber, but since it sometimes returns an OpInt I think the way you’ve written it is probably the simplest

  • doTheSubtraction can potentially be simplified. Assuming concat in your code could be replaced by append, then doTheSubtraction a b is the same as toEither (sub <$> V a <*> V b). This uses the Validation applicative to handle the error logic (ie, what to do with Left) and deferrs to sub for the success logic (ie, what to do with Right).

  • I suspect that subtract and makeOperate can be improved as well (their use of Maybe looks suspiecious to me), but it’s hard to tell without more context

Thanks! This is helpful. The current version of this (alpha) code is here: operations-ps/src/Sitebender.purs at main · site-bender/operations-ps · GitHub

I’ll take a look at doTheSubtraction. I have been looking at V already.

What I was curious about was whether sub or doTheSubtraction could be folded into subtract – one function vs. three. But maybe that would just overcomplicate things.

The use of Maybe in makeOperate is because it returns a function (to be called from JS) which has an optional parameter which might be used in the calculation, or might not.

Thank you much for your help. Sounds like I’m not as far off as I thought I might be.

1 Like

doTheSubtraction can definitely be folded into subtract:

subtract (SubtractOperand r) v = do
  m <- makeOperate r.minuend v
  s <- makeOperate r.subtrahend v
  pure <<< toEither $ sub <$> V m <*> V s

I wouldn’t recommend inlining sub the same way. However, you might be interested in factoring out the type-conversion logic:

converting ::
  (Int -> Int -> Int)              -- What to do on int
  -> (Number -> Number -> Number)  -- What to do on float
  -> OpResult -> OpResult -> OpResult
converting f g =
  case _, _ of
    OpInt x, OpInt y -> f x y
    x, y -> g (toNumber' x) (toNumber' y)

  where
  toNumber' = case _ of
    OpNumber a -> a
    OpInt a -> toNumber a

Then you could write subtract as

subtract (SubtractOperand r) v = do
  m <- makeOperate r.minuend v
  s <- makeOperate r.subtrahend v
  let minus = converting (-) (-)
  pure <<< toEither $ minus <$> V m <*> V s
1 Like

Outstanding! Thanks much. Not only does this work, but I actually understand it.

add :: AddOperation -> Maybe OpResult -> Effect (Either (Array String) OpResult)
add (AddOperation r) v = do
  m <- makeOperate r.leftAddend v
  s <- makeOperate r.rightAddend v
  let plus = converting (+) (+)
  pure <<< toEither $ plus <$> V m <*> V s

The only thing I still don’t get is how I could change this to take a single “addends” foldable and then use foldl and zero to add two or more addends.

Here is where I go wrong. I just can’t quite wrap my head around (pun intended) unwrapping all these contexts. This doesn’t work:

type AddOpRow r = (addends :: (Array Operation) | r)
newtype AddOperation = AddOperation (Record (AddOpRow ()))

add :: AddOperation -> Maybe OpResult -> Effect (Either (Array String) OpResult)
add (AddOperation r) v = do
  let plus = converting (+) (+)
  let sum l r = plus <$> V (makeOperate l v) <*> V (makeOperate r v)
  pure <<< toEither $ foldl plus r.addends

The problem, of course, is that (makeOperate l v) and (makeOperate r v) return Effect. I can’t figure out how to make this work. I tried lift2, which seems right, but then I get the V issue (Effect vs. Either t0):

add (AddOperation r) v = do
  let plus = converting (+) (+)
  let sum l r = lift2 plus (V (makeOperate l v)) (V (makeOperate r v))
  pure <<< toEither $ foldl sum zero r.addends

Hopefully, all this wrapping and unwrapping will eventually work its way into my brain and it will become second nature. Hopefully …

Your new AddOperation is isomorphic to Array Operation, so I’m going to just use that for simplicity

It is indeed troublesom that makeOperate returns an Effect. It is extra annoying that it returns an Effect (Either _ _). We can deal with these by using the monadic map function:

Data.Traversable.traverse :: (a -> Effect b) -> Array a -> Effect (Array b)

as well as

Data.Traversable.sequence :: Array (Either e a) -> Either e (Array a)

(Presented with simplified types)

One way to write your variadic add is like this:

add :: Array Operation -> Maybe OpResult -> Effect (Either (Array String) OpResult)
add ops v = do
  (addendsAE :: Array (Either _ _)) <- traverse (\op -> makeOperate op v) ops
  (addendsEA :: Either _ (Array _)) = toEither (sequence (V <$> addendsEA))
  let plus = converting (+) (+)
  pure $ (foldl plus zero <$> addendsEA)

(untested)

Traverse. Sigh, of course. I was actually using that in the TS version of this code.

I’ll work through this. Thanks.

The reason for the Effect is that at the bottom of the tree of operations are “injectors” that get the values (operands) from form fields, session storage, etc. Hence, Effect (soon Aff).

1 Like