eigen_enthused
eigen_enthused

Reputation: 535

Undo records in database

I am creating a system that needs to store all the functions and parameters a user has run in a database. No records are ever deleted, but I need to be able to recreate the minimal function sequence and parameter set for deterministic regeneration.

The users interaction is very minimal, they are not programming - input interaction is handled in C++ is passed through the FFI as data to accumulate into lists and callback to process the current buffer of data. The function triggers a series of decisions on how to wire a processing graph of sets of data within the database, and functions they are input to. The graph is acyclic. This graph is initially run and values are visualized for the user. Later portions of the graph will be recombined to generate new graphs.

Haskell internal construction of these graphs is created from analysis of data in the database and simple random choices amongst combinations. I'd like to be able to just store a seed of a random generator, the module and parameter id to which it applies.

I think this may be best framed as storing the functions of a EDSL in a database, where only the highlevel interaction is stored but is fully deterministic.

I am not interested in storing the values, but rather the function graph of the action.

Each table refers to different function. Each record has a date and a task ID to group all the functions of specific actions to gether. The parameters reference a Table ID and Record ID. If a composed function is internally doing something like generating a random number, the seed for that number should be automatically stored.

I am using GHC stage 1 with no GHCI and Persistent SQlite.

I am still new to Haskell and am looking to find out what approach and packages would be appropriate for tackling this problem in a functional manner.

Upvotes: 1

Views: 121

Answers (1)

luqui
luqui

Reputation: 60463

If you want to do this for source-level functions, such as:

myFoo x y = x + y

you are pretty much out of luck, unless you want to go hacking around in the compiler. However, you could define your own notion of function that does support this, with some suitable annotations. Let's call this notion a UserAction a, where a is the return type of the action. In order to compose computations in UserAction, it should be a Monad. Not thinking too awfully hard, my first impression would be to use this stack of monad transformers:

type UserAction = WriterT [LogEntry] (ReaderT FuncIdentifier IO)

The WriterT [LogEntry] component says that a UserAction, when run, produces a sequence of LogEntrys [1], which contain the information you want to write to the database; something like:

data LogEntry = Call FuncIdentifier FuncIdentifier

It's okay to put off storing the random seed, task identifier, etc. for now -- that can be incorporated into this design by adding information to LogEntry.

The ReaderT FuncIdentifier component says that a UserAction depends on a FuncIdentifier; namely, the identifier of the function that is calling it.

FuncIdentifier could be implemented by something as simple as

type FuncIdentifier = String

or you use something with more structure, if you like.

The IO component says that UserActions can do arbitrary input and output to files, the console, spawn threads, the whole lot. If your actions don't need this, don't use it (use Identity instead). But since you mentioned generating random numbers, I figured you did not have pure computations in mind[2].

Then you would annotate each action you want to record logs for with a function like this:

userAction :: FuncIdentifier -> UserAction a -> UserAction a

which would be used like so:

randRange :: (Integer, Integer) -> UserAction Integer
randRange (low,hi) = userAction "randRange" $ do
    -- implementation

userAction would record the call and set up its callees to record their calls; e.g. something like:

userAction func action = do
    caller <- ask
    -- record the current call
    tell [Call caller func]
    -- Call the body of this action, passing the current identifier as its caller.
    local (const func) action

From the top level, run the desired action and after it has finished, collect up all the LogEntrys and write them to the database.

If you need the calls to be written in real time as the code is executing, a different UserAction monad would be needed; but you could still present the same interface.

This approach uses some intermediate Haskell concepts such as monad transformers. I suggest going on IRC to irc.freenode.net #haskell channel to ask for guidance on filling out the details of this implementation sketch. They are a kind bunch and will happily help you learn :-).

[1] In practice you will not want to use [LogEntry] but rather DList LogEntry for performance. But the change is easy, and I suggest you go with [LogEntry] until you get more comfortable with Haskell, then switch over to DList.

[2] Random number generation can be done purely, but it takes further brain-rewiring which this sketch already has plenty of, so I suggest just treating it as an IO effect for the purpose of getting going.

Upvotes: 3

Related Questions