purescript-contrib / purescript-profunctor-lenses

Pure profunctor lenses
MIT License
144 stars 51 forks source link

Adds "monadic" lenses and prisms #129

Open mikesol opened 3 years ago

mikesol commented 3 years ago

This is a draft PR to create utilities for working with monads in lenses.

It currently covers _1M, _2M, _LeftM and _RightM. I'd like to do traversed and propM as well, after which I think it'll be in good shape for review. propM seems manageable but traversedM, with signature traversedM :: forall t a b m. Traversable t => Traversal (t a) (m (t b)) a (m b), seems quite daunting. Essentially you'd need to "apply traverse twice" but I'm not sure if that's possible and what that function would look like. If anyone would be interested in writing traversedM that'd be helpful!

Checklist:

LiamGoodacre commented 3 years ago

I don't think I've seen optics used in this way before, do you have any example usages? I'm wondering if they relate at all to https://hackage.haskell.org/package/lens-action

mikesol commented 3 years ago

I don't think I've seen optics used in this way before, do you have any example usages?

The hackage package looks interesting, and the signatures seem similar.

The example comes from my current sketches for the next iteration of https://github.com/mikesol/purescript-audio-behaviors. In it, the previous audio graph g0 (ie the one 20 milliseconds before) is passed to a function that creates the next audio graph. This is done so that the parts of g0 that don't change aren't rebuilt, which greatly speeds up rendering.

g0 is a monad where the actual audio graph is stashed in the monad and the terms a user can get out of the monad (ie an individual sine wave) are just pointers to nodes in the graph. That means that, as we traverse down the graph to get an individual element (ie an individual sine wave oscillator), we are actually getting pointers. These pointers are uninteresting: they are only useful insofar as they can be used to manipulate or query the graph. But it is important that we can traverse down the graph to get the pointer to any node.

To do this, I wound up writing a bunch of functions that produced stuff like Lens (f s) (f t) (f a) (f b), where f is the monad containing the graph and s t a b are various pointers in the graph (ie s and t would be a loudspeaker and a and b would be a single sine wave in the graph). My first step was looking around pursuit to see if these functions already existed, but they do not, which leads me to believe that they're either (a) too specialized; (b) a bad idea; or (c) valuable. If it is (c), then perhaps others would use them as well, and as implementing them is a pain (it requires understand profunctor optics, which took me months), I thought it may be worth it to contribute them to this repo instead of making my own.

I see that the hackage package is separate from the main lens one, so maybe these functions are too exotic to be in the profunctor-lenses repo, in which case I can make a new project and we can merge the functions here if/when they get more traction/mindshare. Let me know!

mikesol commented 3 years ago

Another example from our code base at Meeshkan:

remove :: Int -> Script8Base -> Script8Base
remove sIndex script = widthAdjustedSIndex
  where
  items' = preview items script

  withModifiedCount = set count (maybe 0 (add (-1) <<< A.length) items') script

  withRemovedCommand = over items (\x -> fromMaybe x (A.deleteAt sIndex x)) withModifiedCount

  widthAdjustedSIndex = over items (A.mapWithIndex (\i a -> a { sIndex = i })) withRemovedCommand

Here, the fromMaybe is a hack if sIndex is out of the boundaries of array x. What it should do is bubble up an error message to the top of the lens. Monadic lenses would allow for this. The signature would be ie remove :: forall e. e -> Int -> Script8Base -> Either e Script8Base.

twhitehead commented 3 weeks ago

Maybe a silly question, as I don't fully understand what you are doing, but

Maybe then you might find some solution in looking at creating optic using functions too? That is, could you create a lens using functions to achieve what you are wanting instead? In other words, come up with a profunctor to instantiate optics in a way to achieve what you are wanting.

For example, traverseOf instantiates the profunctor p to Star f. That is p a b = Star f a b = a -> f b. This is the classic monadic action term, and it means the optic is instantiated to

Optic p s t a b
= p a b -> p s t
= Star f a b -> Star s t 
= (a -> f b) -> (s -> f t)

which is very much looking like the sort of thing you were thinking about when s = v a and t = v b for some container (vessel) v

= (a -> f b) -> (v a -> f (v b))

This is also the classic container traversal definition for some applicative f.

So, if f is your monad (which is applicative), and v a is your container, this will, given a value to action mapping a -> f b, return an action for a container of results f (v b) obtained by mapping the container values a to actions f b using your a -> f b, sequencing those actions in order, and collecting the results in a new container v b.