Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Separation of data loading/unloading and processing logic

Sometimes it is necessary to perform some complex routines in order to retrieve or save data, which is being processed. In this case one wants to separate data generation and data processing logic. The common way is to use iteratee-like functionality. There are lots of decent libraries: pipes, conduit, etc. In most cases they will do the thing. But AFAIK they are (except, maybe, pipes) limited by the order of processing.

But consider a log viewer example: human may desire to ramble back and forth randomly. He also may zoom in and out. I fear iteratees can't help here.

A straightforward solution may look like this:

-- True is for 'right', 'up', etc. and vice versa 
type Direction = Bool

class Frame (f :: * -> *) where
  type Dimension f :: *

  type Origin f :: * -> *

  grow', shrink' move' :: Monad m => Dimension f -> Direction -> f a -> m (f a)

  move' dim dir f = grow' dim dir f >>= shrink' dim (not dir)

  liftF' :: (Origin f a -> b) -> f a -> b

class Frame f => MFrame f where
  liftMF' :: (Origin f a -> (b, Origin f a)) -> f a -> (b, f a)

-- Example instance: infinite stream.
data LF a = LF [a] [a] [a]

instance Frame LF where
  type Dimension LF = () -- We have only one dimension to move in...
  type Origin LF = [] -- User see piece of stream as a plain list

  liftF' f (LF _ m _) = f m

  grow' () True (LF l m (h:r)) = return $ LF l (m++[h]) r
  ...

Then one may wrap this into StateT and so on. So, the questions:

0) Did I miss the point of iteratees completely, and they are applicable here?

1) Did I just reinvent a well-known wheel?

2) It is obvious, that grow and shrink operations are pretty uneffective, as their complexity is proportional to the frame size. Is there a better way to extend zippers like this?

like image 554
user3974391 Avatar asked Mar 21 '14 07:03

user3974391


1 Answers

You want lenses, specifically the sequenceOf function. Here is an example of targeted loading of a 3-tuple:

 sequenceOf _2 :: (IO a, IO b, IO c) -> IO (IO a, b, IO c)

sequenceOf takes a lens to a polymorphic field that contains a loading action, runs the action, then replaces the field with the result of the action. You can use sequenceOf on your own custom types by just making your type polymorphic in the fields you want to load, like this:

data Asset a b = Asset
    { _art :: a
    , _sound :: b
    }

... and also making your lenses use the full four type parameters (this is one reason why they exist):

art :: Lens (Asset a1 b) (Asset a2 b) a1 a2
art k (Asset x y) = fmap (\x' -> Asset x' y) (k x)

sound :: Lens (Asset a b1) (Asset a b2) b1 b2
sound k (Asset x y) = fmap (\y' -> Asset x y') (k y)

... or you can auto generate lenses using makeLenses and they will be sufficiently general.

Then you can just write:

sequenceOf art :: Asset (IO Art) b -> IO (Asset Art b)

... and loading multiple assets is as simple as composing Kleisli arrows::

sequenceOf art >=> sequenceOf sound
    :: Asset (IO Art) (IO Sound) -> IO (Asset Art Sound)

... and of course you can nest assets and compose lenses to reach nested assets and everything still "just works".

Now you have a pure Asset type that you can process using pure functions, and all the loading logic is factored out into lenses.

I wrote this on my phone so there may be several errors, but I will fix them later.

like image 86
Gabriella Gonzalez Avatar answered Oct 21 '22 11:10

Gabriella Gonzalez