I would like to define something like a lens, but which can fail when trying to set. See fooLens in the following example.
{-# LANGUAGE RankNTypes #-}
import Data.Char (toUpper)
import Data.Functor.Const (Const(..))
import Data.Functor.Identity (Identity(..))
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type Getting r s t a = (a -> Const r a) -> s -> Const r t
view :: Getting a s t a -> s -> a
view l = getConst . l Const
over :: Lens s t a b -> (a -> b) -> s -> t
over l f = runIdentity . l (Identity . f)
data Foo a = Foo a deriving (Show)
fooLens :: Lens (Foo a) (Either String (Foo a)) a a
fooLens f (Foo a) = Right . Foo <$> f a
main = do
let foo = Foo "test"
print foo
print $ view fooLens foo
print $ over fooLens (map toUpper) foo
The output of this is what you would expect
Foo "test"
"test"
Right (Foo "TEST")
I have generalised the definition of Getting here to make this work. The first thing to make clear is that fooLens is not a lens: it doesn't satisfy the lens laws. Instead, it is the composition of a lens and something like a prism.
This seems to work, but the fact that it's not supported by any of the lens libraries I've checked suggests that there may be a better way to go about this problem. Is there a way to refactor fooLens so that it:
- Acts as a getter, i.e. it can always retrieve a value.
- Can act as a setter with the possibility of failing, for example it returns an Either.
Your specific formulation doesn't work very well within the lens ecosystem. The most important thing lens does is provide composition of optics of different types. To demonstrate, let's start with a slightly embellished version of your code:
The output is:
I modified
fooLensa bit to take full advantage of its type, validating data on update. This helps to illustrate the goal with this formulation.Then I decided to test out how well this composes, and added the following:
And then adding the following to
main:It just doesn't compose:
This alone is enough to prevent using this formulation in lens. It doesn't fit within the goals of the library.
Let's try something different. This isn't exactly what you're looking for, but it's an observation.
Output:
Obviously that's not a true lens and should probably have a different name, as it doesn't obey the set-view law. It's a bit awkward that it can be written with the same type, but there's precedent for that with things like
filtered.But there's a further complication, as evidenced by the last test - filtering on the result of an update still requires running the update's effects, even when the update is rejected. That's not how skipping an element, with
filteredfor instance, in aTraversalworks. That seems like it's impossible to avoid with the van Laarhoven representation. But maybe that's not so bad. It isn't an issue when setting or viewing - only when doing much less common operations.In any case, it doesn't report the failure to set, so it's not exactly what you're looking for. But with enough rejiggering, it can be a starting point.
And here's the output from running it:
This formulation integrates with normal lens stuff. It works, at the expense of requiring a variation on
overto get the error reporting. It maintains compatibility with a lot of lens functions, at the cost of a bit of non-lawful behavior in one case. It's not perfect, but it's probably as close as you can get within the constraints of maintaining compatibility with the rest of the lens library.As for why something along these lines isn't in the library, it's probably because it requires a custom constraint on the
ftype alias, which is a real hassle for working with combinators like(%%~). The instances I provided forIdentityandConsttake care of most uses from lens itself, but there's a more people might choose to do with it.The lens library's open design allows for a huge amount of external customization. This is a possible approach that probably works for a lot of cases. But it works for a lot less than the full breadth of what lens allows, and I think that's why nothing like this is currently present.