Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating elements of multiple collections with dynamic functions

Setup:

I have several collections of various data structures witch represent the state of simulated objects in a virtual system. I also have a number of functions that transform (that is create a new copy of the object based on the the original and 0 or more parameters) these objects.

The goal is to allow a user to select some object to apply transformations to (within the rules of the simulation), apply those the functions to those objects and update the collections by replacing the old objects with the new ones.

I would like to be able to build up a function of this type by combining smaller transformations into larger ones. Then evaluate this combined function.

Questions:

How to I structure my program to make this possible?

What kind of combinator do I use to build up a transaction like this?

Ideas:

  1. Put all the collections into one enormous structure and pass this structure around.
  2. Use a state monad to accomplish basically the same thing
  3. Use IORef (or one of its more potent cousins like MVar) and build up an IO action
  4. Use a Functional Reactive Programing Framework

1 and 2 seem like they carry a lot of baggage around especially if I envision eventually moving some of the collections into a database. (Darn IO Monad)

3 seems to work well but starts to look a lot like recreating OOP. I'm also not sure at what level to use the IORef. (e.g IORef (Collection Obj) or Collection (IORef Obj) or data Obj {field::IORef(Type)} )

4 feels the most functional in style, but it also seems to create a lot of code complexity without much payoff in terms of expressiveness.


Example

I have a web store front. I maintain a collections of products with (among other things) the quantity in stock and a price. I also have a collection of users who have credit with the store.

A user comes along ands selects 3 products to buy and goes to check out using store credit. I need to create a new products collection that has the amount in stock for the 3 products reduced, create a new user collection with the users account debited.

This means I get the following:

checkout :: Cart -> ProductsCol -> UserCol -> (ProductsCol, UserCol)

But then life gets more complicated and I need to deal with taxes:

checkout :: Cart -> ProductsCol -> UserCol -> TaxCol 
            -> (ProductsCol, UserCol, TaxCol)

And then I need to be sure to add the order to the shipping queue:

checkout :: Cart 
         -> ProductsCol 
         -> UserCol 
         -> TaxCol
         -> ShipList
         -> (ProductsCol, UserCol, TaxCol, ShipList)

And so forth...

What I would like to write is something like

checkout = updateStockAmount <*> applyUserCredit <*> payTaxes <*> shipProducts
applyUserCredit = debitUser <*> creditBalanceSheet

but the type-checker would have go apoplectic on me. How do I structure this store such that the checkout or applyUserCredit functions remains modular and abstract? I cannot be the only one to have this problem, right?

like image 793
John F. Miller Avatar asked Jan 19 '23 01:01

John F. Miller


1 Answers

Okay, let's break this down.

You have "update" functions with types like A -> A for various specific types A, which may be derived from partial application, that specify a new value of some type in terms of a previous value. Each such type A should be specific to what that function does, and it should be easy to change those types as the program develops.

You also have some sort of shared state, which presumably contains all the information used by any of the aforementioned update functions. Further, it should be possible to change what the state contains, without significantly impacting anything other than the functions acting directly on it.

Additionally, you want to be able to abstractly combine update functions, without compromising the above.

We can deduce a few necessary features of a straightforward design:

  • An intermediate layer will be necessary, between the full shared state and the specifics needed by each function, allowing pieces of the state to be projected out and replaced independently of the rest.

  • The types of the update functions themselves are by definition incompatible with no real shared structure, so to compose them you'll need to first combine each with the intermediate layer portion. This will give you updates acting on the entire state, which can then be composed in the obvious way.

  • The only operations needed on the shared state as a whole are to interface with the intermediate layer, and whatever may be necessary to maintain the changes made.

This breakdown allows each entire layer to be modular to a large extent; in particular, type classes can be defined to describe the necessary functionality, allowing any relevant instance to be swapped in.

In particular, this essentially unifies your ideas 2 and 3. There's an inherent monadic context of some sort here, and the type class interface suggested would allow multiple approaches, such as:

  • Make the shared state a record type, store it in a State monad, and use lenses to provide the interface layer.

  • Make the shared state a record type containing something like an STRef for each piece, and combine field selectors with ST monad update actions to provide the interface layer.

  • Make the shared state a collection of TChans, with separate threads to read/write them as appropriate to communicate asynchronously with an external data store.

Or any number of other variations.

like image 126
C. A. McCann Avatar answered Feb 01 '23 08:02

C. A. McCann