Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds "monadic" lenses and prisms #129

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

mikesol
Copy link

@mikesol mikesol commented Mar 22, 2021

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:

  • Added the change to the changelog's "Unreleased" section with a link to this PR and your username
  • Linked any existing issues or proposals that this pull request should close
  • Updated or added relevant documentation in the README and/or documentation directory
  • Added a test for the contribution (if applicable)

Comment on lines 109 to 114
element ::
forall p s t a.
Wander p =>
Int ->
Traversal s t a a ->
Optic p s t a a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind preserving the existing formatting? That'll just help this PR stay easy to review and if we switch to arrow-last style we can apply it in one commit across the repository.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, is there a vs code setting that can bring that formatting in? Apologies for the clutter!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the formatting back now 👍
I also marked it ready for review.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We haven’t formally adopted a formatter, so in these libraries it’s mostly about following the existing formatting until we do adopt one. Thanks!

@mikesol mikesol marked this pull request as ready for review March 22, 2021 21:28
@LiamGoodacre
Copy link
Member

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
Copy link
Author

mikesol commented Mar 25, 2021

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
Copy link
Author

mikesol commented May 27, 2021

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
Copy link
Contributor

twhitehead commented Aug 29, 2024

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

  • the optic using functions (e.g., view) rely on being able it instantiate the profunctor with an appropriate instance to achieve the desired result (e.g., holding a spot to collect results in, etc.), while
  • the optic creation functions (e.g., traversed) work by giving you a term that can be instantiated with the most general profunctor possible in order to be compatible with as many as the using functions (i.e., instantiable to their profunctors) as possible.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants