What is the cleanest way of updating an item in a list

Assume a list of “cash records”. The cash records have a currency and an amount field.
Now I want to write a function add that takes a cash record, and a list of cash records and does the following:

  1. if there is an entry with the same currency, update the existing list by adding the amounts
  2. if there is no entry with the same currency, append it to the list.

I’ve written the following code, but I’m unsure about the elegance of replace

    case find (\cash -> cash.currency == newCash.currency) cashList of
        Some a -> 
            replace 
                [a] 
                [newCash]
                cashList
        None -> cashList <> [newCash]

Any Ideas?

1 Like

Your solution works just fine, but worst case does two passes through the list. One to find the element and another to replace it.

In general, if you have a list where you want a key, in this case currency to be unique, I’d recommend using a map. TextMap would work really well for this.

If you want to stick with a list, I’d recommend one of three approaches for a functions like this: using the break function, using recursion, or using a fold:

data Cash = Cash with
  currency : Text
  amount : Decimal
    deriving (Eq, Show)
-- Using `break`
addCash(newCash: Cash, cashList: [Cash]): [Cash] =
  let
    (h, t) = break (\cash -> cash.currency == newCash.currency) cashList
    newt = case t of
      c::t' -> (c with amount = c.amount + newCash.amount)::t'
      [] -> [newCash]
  in h ++ newt
-- Using recursion
addCash'(newCash: Cash, cashList: [Cash]): [Cash] =
  case cashList of
    [] -> [newCash]
    c::t -> if c.currency == newCash.currency
      then (c with amount = c.amount + newCash.amount)::t
      else c :: addCash' (newCash, t)
-- using a fold
addCash''(newCash: Cash, cashList: [Cash]): [Cash] =
  let
    workfn c (processed, t) = if c.currency == newCash.currency
      then (True, (c with amount = c.amount + newCash.amount)::t)
      else (processed, c::t)
    (processed, t) = foldr workfn (False, []) cashList
  in if processed then t else newCash::t
test_addCash = script do
  let
    cashList =
      [ Cash with currency = "A"; amount = 100.0
      , Cash with currency = "B"; amount = 100.0
      , Cash with currency = "C"; amount = 100.0]
    newCash = Cash with currency = "B"; amount = 100.0
    newCash' = Cash with currency = "D"; amount = 100.0
    cashList' = [ Cash with currency = "A"; amount = 100.0
      , Cash with currency = "B"; amount = 200.0
      , Cash with currency = "C"; amount = 100.0]
    cashList'' =
      [ newCash'
      , Cash with currency = "A"; amount = 100.0
      , Cash with currency = "B"; amount = 100.0
      , Cash with currency = "C"; amount = 100.0]
    cashList''' =
      [ Cash with currency = "A"; amount = 100.0
      , Cash with currency = "B"; amount = 100.0
      , Cash with currency = "C"; amount = 100.0
      , newCash']
  addCash (newCash, cashList) === cashList'
  addCash (newCash', cashList) === cashList'''
  addCash' (newCash, cashList) === cashList'
  addCash' (newCash', cashList) === cashList'''
  addCash'' (newCash, cashList) === cashList'
  addCash'' (newCash', cashList) === cashList''
3 Likes

For completeness, here is a version using TextMap. As a bonus, you get commutativity on addition and equality on stashes of cash independent of currency-insertion order.

module Main where

import Daml.Script
import qualified DA.TextMap as TextMap
import DA.TextMap (TextMap)

data Cash = Cash (TextMap Decimal)
    deriving (Eq, Show)

(===) : Eq a => a -> a -> Script ()
(===) a b = assert (a == b)

cash : Text -> Decimal -> Cash
cash cur amount =
  Cash $ TextMap.fromList [(cur, amount)]

addCash : Cash -> Cash -> Cash
addCash (Cash cash1) (Cash cash2) =
  Cash $ TextMap.merge
    (\cur amount -> Some amount) -- don't change curs only in cash1
    (\cur amount -> Some amount) -- don't change curs only in cash2
    (\cur amount1 amount2 -> Some (amount1 + amount2)) -- add together amounts for curs in both
    cash1
    cash2

test_addCash = script do
  let
    stash = Cash $ TextMap.fromList [("A", 100.0), ("B", 100.0), ("C", 100.0)]
    b = cash "B" 100.0
    d = cash "D" 100.0
    stashWithB = Cash $ TextMap.fromList [("A", 100.0), ("B", 200.0), ("C", 100.0)]
    stashWithD = Cash $ TextMap.fromList [("A", 100.0), ("B", 100.0), ("C", 100.0), ("D", 100.0)]
    stashWithBoth = Cash $ TextMap.fromList [("A", 100.0), ("B", 200.0), ("C", 100.0), ("D", 100.0)]
  addCash (cash "A" 100.0) (addCash (cash "B" 100.0) (cash "C" 100.0)) === stash
  addCash stash b === stashWithB
  addCash b stash === stashWithB
  addCash d stash === stashWithD
  addCash b (addCash stash d) === stashWithBoth
  addCash d (addCash stash b) === stashWithBoth
  addCash stash (addCash b d) === stashWithBoth
  addCash (addCash d b) stash === stashWithBoth
4 Likes