Skip to content
Switch branches/tags
Go to file
Cannot retrieve contributors at this time
1002 lines (691 sloc) 38.1 KB

% lens over tea #4: isomorphisms, some profunctors, lens families

series: top: “lens over tea” toplink: /#lens-over-tea prev: /lens-over-tea-3 next: /lens-over-tea-5 well as human sacrifice, dogs and cats living together, mass hysteria— ahem. There'll be prisms in the next post, but first we'll have to understand isomorphisms, because isomorphisms are easier and prisms kinda follow from isomorphisms.


  • I'm tempted to ditch prisms and spend several days figuring out pure profunctor lenses instead (yes I'll explain what it means in this very post). Apparently currently knowledge of profunctor lenses is being passed orally on #haskell-lens and nowhere else (not counting this post which isn't much more than a bunch of functions and types), and I hate the fact that there exist things which I can't learn without having to talk to someone else.

  • Thanks to ion on #haskell-lens for explaining things and making it click for me (primarily by showing me Tagged and explaining what Choice is about). (This doesn't contradict the previous point – I'm still annoyed that to figure out prisms I had to talk to someone instead of, say, just consulting lens docs.)

  • I wonder how much upvotes will this part get, because on one hand the trend is rather bad (622518), but on the other hand this one has a more interesting-sounding title (I think?), so... we'll see. Yeah, I'm a sucker for upvotes.

  • Here's a cat video for you (watch with sound).

Lens families

There's a thing about polymorphic lenses which I haven't told you before because I didn't know about it.

Here's a polymorphic lens:

type Lens s t a b

We know the following about such lenses:

  • a is a part of s
  • b is a part of t
  • when you replace a in s with b, its type changes to t

(Just in case: “a is a part of s” doesn't necessarily mean that s looks like Maybe a or something else of the shape g a. It can perfectly be (a, b), for instance, which can't be represented as a g a because Haskell lacks type-level lambdas (for the same reason you wouldn't be able to e.g. make (a, a) a functor). Moreover, there are lenses like united (of type Lens' s ()), which work for any s, no matter whether () is contained in s as a type parameter or not. So, “a is a part of s” should be understood as something conceptual and not literal.)

This intuitive understanding of polymorphic lenses is kinda formalised in this post:

So, why do I use the term “lens family” rather than “polymorphic lens”?

In order for the lens laws to hold, the 4 types parameterizing our lens family must be interrelated.

In particular you need to be able to put back (with .~) what you get out of the lens (with ^.) and put multiple times.

This effectively constrains the space of possible legal lens families to those where there exists an index kind i, and two type families outer :: i -> *, and inner :: i -> *. If this were a viable type signature, then each lens family would actually have 2 parameters, yielding something like:

-- pseudo-Haskell
type LensFamily outer inner =
  forall a b. Lens (outer a) (outer b) (inner a) (inner b)

This forall a b there implies that you should be able to swap pairs of types and nothing should change. See for yourself:

  • _1 :: Lens (a, x) (b, x) a b
    _1 :: Lens (b, x) (a, x) b a
  • _Left :: Prism (Either a c) (Either b c) a b
    _Left :: Prism (Either b c) (Either a c) b a
  • traversed :: Traversable f => Traversal (f a) (f b) a b
    traversed :: Traversable f => Traversal (f b) (f a) b a

In each pair, both types are completely the same because the only thing that's different is variable names, and renaming a type variable doesn't change the type.

If you have a weird lens like this:

Lens [Int] [Bool] (Maybe Char) String

it'll never satisfy any lens laws, because they won't even typecheck (like, “you get what you put in”, yeah, except that you get Maybe Char and you put in String and they don't even have equality defined on them).

I'm not sure whether such lenses can ever be useful – the inner/outer law only restricts your setter, not your getter, so you'd only need to throw it away if you needed a weirder setter— I'll just make up an example, y'know.

Let's say you have strict Text and you want to simultaneously convert it to lazy Text and modify some characters:

import qualified Data.Text as Strict
import qualified Data.Text.Lazy as Lazy

strictToLazy :: Traversal Strict.Text Lazy.Text Char Char

It looks like “simultaneously convert and modify” is this traversal's primary purpose. Why would you want to use it as a getter, then? You would use each as a generic getter/setter, and you would only use strictToLazy when you actually want to do this convert-and-modify thing, and then strictToLazy can be a setter instead.

But actually, I don't know. Let's move on to isomorphisms.


(Isos are defined in Control.Lens.Iso.)

“Iso” is a shortening of “isomorphism”; an Iso' s a:

  • is a lens which lets you access a in s
  • is a lens which lets you access s in a (when inverted)
  • is isomorphic to (s -> a, a -> s)

So, if you have an Iso' s a, it means that you can convert between s and a without losing any information.

An Iso s t a b is a generalisation that also lets you change the types:

Iso s t a b ~ (s -> a, b -> t)

over someIso        :: (a -> b) -> (s -> t)
over (from someIso) :: (t -> s) -> (b -> a)
view someIso        :: s -> a
view (from someIso) :: b -> t

And if you assume that the iso follows the inner/outer law and types can be safely swapped:

over someIso        :: (a -> b) -> (s -> t)
over (from someIso) :: (s -> t) -> (a -> b)
view someIso        :: s -> a
view (from someIso) :: a -> s

Now, as an example, let's take enum. Previously you could've said it was a lens to “access” the value corresponding to a number:

> 88 ^. enum :: Char

> 88 & enum %~ toLower

> fromEnum 'x'

However, you can also use it to convert things in the other direction:

> 0 & enum .~ 'x'

We were able to do it because we could create 0 out of thin air. In fact, we could even use undefined and enum wouldn't care:

> undefined & enum .~ 'x'

Iso generalises this notion of lenses that don't care.

An obvious approach

If we want isos to work in both directions, we just need some type of “bidirectional function” which could work in either direction. Then constructing and inverting isos would be trivial:

type Iso s t a b =
  forall f. Functor f => (a -> f b) <-> (s -> f t)

In Haskell, when you want something to be several things at once, you define a class (1 can be both Int and Double thanks to Num, empty can be both [] and Nothing thanks to Alternative, etc). So, let's create a class for bidirectional functions:

class Isomorphic k where
  isomorphic :: (a -> b) -> (b -> a) -> k a b

Now, if -> is an instance of Isomorphic, then functions created with isomorphic would be usable as ordinary functions:

instance Isomorphic (->) where
  -- we just don't need the other direction
  isomorphic f _ = f

You might think that now we would write an instance for <- (well, <- doesn't exist, so we'd have to write an instance for Op really, but still). But we won't – there's really no reason to bother with <- when we can just store both directions:

data Isomorphism a b = Isomorphism (a -> b) (b -> a)

instance Isomorphic Isomorphism where
  isomorphic = Isomorphism

With Isomorphism, it's much easier to write a function that would reverse an iso:

-- from :: Isomorphism a b -> Isomorphism b a
-- from :: Isomorphism a b -> (b -> a)
from :: Isomorphic k => Isomorphism a b -> k b a
from (Isomorphism a b) = isomorphic b a

Finally, we make Iso itself a bidirectional function:

type Iso s t a b =
  forall k f. (Isomorphic k, Functor f) =>
  k (a -> f b) (s -> f t)

And you can create an iso from functions the easy way – just make 2 lenses going in opposite directions:

isos :: (s -> a) -> (a -> s)      -- s <-> a
     -> (t -> b) -> (b -> t)      -- t <-> b
     -> Iso s t a b
isos sa as tb bt = isomorphic
  (\afb s -> bt <$> afb (sa s))   -- easy peasy
  (\sft a -> tb <$> sft (as a))   -- lemon squeezy

That's all. Since Iso uses the Isomorphic class, it would return a function when we apply ^. to it, and it would return a nice pair of functions when we want to invert it with from. This is exactly how things were done in lens 3.6. You have just learned a piece of ancient Haskell!

This approach has a flaw, however: if you compose isos with ., they'll turn into ordinary functions and the result will be an ordinary function as well (while it could still be an iso). We can preserve both directions by writing an instance of Category for Isomorphism and using the . from Control.Category, but then we could just use it for lenses themselves and forget about all the pains we took to make them composable with ordinary ..

(It also has another flaw: you need to give isos 4 functions, but you could do with just 2. Ugh, inelegant.)

A better approach

If we want to do better, we can't really do anything with -> between (a -> f b) and (s -> f t) – it'll be lost when we try to compose isos. We also won't achieve anything by placing constraints on f. What to do, what to do?

Well, there's only 1 thing left to meddle with – ->s in parens. Let's meddle!

In fact, we could even use undefined and enum wouldn't care:

> undefined & enum .~ 'x'

Iso generalises this notion of lenses that don't care.

First I want to explain why exactly it doesn't care. As you know, a lens is isomorphic to this:

Lens s t a b ~ (s -> a, s -> b -> t)


Lens s t a b ~ (s -> a, (s, b) -> t)

We can be even more precise if you remember the “hole in the type” approach – a lens decomposes s into (s/a, a) (except that there's no / in Haskell, but whatever) (I wrote s/a instead of s−a because compound types are denoted with “×”, and “+” is for sum types like Either – if you want to know more about algebra of types, I recommend this post):

Lens s t a b ~ (s -> a, s/a×b -> t)

However, if we assume that a is isomorphic to s, there's nothing left for s/a, and it's reduced to 1 (or () in Haskell):

Iso s t a b ~ (s -> a, s/a×b -> t)
Iso s t a b ~ (s -> a,   1×b -> t)
Iso s t a b ~ (s -> a,     b -> t)

In other words, when you take a setter – s -> b -> t – -the part of s that it has to look at to produce the result- equals to (). Or in other other words, it doesn't have to look at s at all – which is why we can set it to undefined and nothing would happen (and which is why the reverse holds as well, and lenses which can be fed undefined are isomorphisms).

Here's how we would've written enum if it was a lens:

enum :: Enum a => Lens' Int a
enum f = \s -> fromEnum <$> f (toEnum s)

Now let's consider 2 cases – the first is when we use it to turn s into a, the second is when we use the iso to turn b into t. The definition we already have works well enough for the former case, but in the latter case s doesn't even exist:

enum f = \_ -> fromEnum <$> f ???

Since f can't possibly get any input, it must be a constant function (and the result of enum is a constant function too). To be able to write safe isomorphisms, we need some way to ensure that f and the result of enum are constant functions – if we don't, how can we be sure that -the function that is the result of enum- won't look at its argument (of type s)?

A constant function of type a -> b is isomorphic to b. We could create our own type for constant functions:

{-# LANGUAGE TypeOperators #-}

-- We don't have to use “:->”, but it looks slighty better than something like
-- “data ConstantFunc a b = ConstantFunc b” and I also wanted to show that
-- this kind of thing is possible.
data a :-> b = Always b

But there's already such a type, called Tagged (in the tagged package):

newtype Tagged a b = Tagged {unTagged :: b}

A Tagged a b value is a value b with an attached phantom type a. This can be used in place of the more traditional but less safe idiom of passing in an undefined value with the type, because unlike an (a -> b), a Tagged a b can't try to use the argument a as a real value.

With Tagged, the definition of enum looks as follows:

enum :: Enum a => Tagged a (f a) -> Tagged Int (f Int)
enum (Tagged fa) = Tagged (fromEnum <$> fa)

However, we have to support ordinary functions too, because there are 2 use cases (s -> a and b -> t) – when we want s -> a, we would give enum an ordinary function, and when we want b -> t, we would give enum a constant function. So, the following 2 definitions must be somehow compatible:

enum (Tagged fa) = Tagged (fromEnum <$> fa)
enum f = \s -> fromEnum <$> f (toEnum s)

Whenever a function can operate on 2 different types in Haskell, it probably means that we'd have to use a typeclass. So, if we want to be able to give enum either an ordinary function or a constant function, we can just create a typeclass for functions:

class IsoFunction p where

type Iso s t a b =
  forall p f. (Functor f, IsoFunction p) =>
  p a (f b) -> p s (f t)

But what methods should that typeclass have? To find out, we have to unify the definitions I gave above:

enum (Tagged fa) = Tagged (fromEnum <$> a)
enum afb = \s -> fromEnum <$> afb (toEnum s)

Okay, let's start unifying. First of all, get rid of the lambda:

enum (Tagged fa) = Tagged (fromEnum <$> fa)
enum f = fmap fromEnum . f . toEnum

Then get rid of explicitly working with Tagged by noticing that:

enum fa = fmap fromEnum <$> retag fa
enum f = fmap fromEnum . f . toEnum

Then, let's observe some parallels:

  • In the case of the ordinary function, fmap fromEnum . changes its output; in the case of the constant function, fmap fromEnum <$> changes its output.

  • In the case of the ordinary function, . toEnum changes its input; in the case of the constant function, retag changes the type of its nonexistent input.

Let's rewrite the functions again to make it more obvious:

enum fa = (fmap fromEnum <$>) $ retag      $ fa
enum f  = (fmap fromEnum .)   $ (. toEnum) $ f

In both cases we apply the same 2 operations: one that changes input, and another that changes output. Well, let's make them the methods of our typeclass:

class IsoFunction p where
  changeInput  :: (s -> a) -> p a b -> p s b
  changeOutput :: (b -> t) -> p a b -> p a t

instance IsoFunction (->) where
  changeInput f = (. f)
  changeOutput f = (f .)

instance IsoFunction Tagged where
  changeInput _ = retag
  changeOutput f = fmap f

(Note that changeInput has to take a function even in case of Tagged – otherwise we wouldn't be able to unify the definitions.)

Now we can define enum:

enum = changeInput toEnum . changeOutput (fmap fromEnum)

And a generic iso function which would create an iso:

iso :: (s -> a) -> (b -> t) -> Iso s t a b
iso sa bt = changeInput sa . changeOutput (fmap bt)

And since we know how to extract both s -> a and b -> t from an iso, we can define from (which inverts an iso):

-- e.g. from :: Enum a => Iso' Int a -> Iso' a Int
from :: Iso s t a b -> Iso b a t s
from i = iso bt sa
    -- This uses the (->) instance:
    --   Сonst   :: a -> Const a b   or  (->) a (Const a b)
    --   i Const :: s -> Const a t   or  (->) s (Const a t)
    sa s = getConst ((i Const) s)

    -- This uses the Tagged instance:
    --   Tagged (Identity b)     :: Tagged a (Identity b)
    --   i (Tagged (Identity b)) :: Tagged s (Identity t)
    bt b = runIdentity . unTagged $ i (Tagged (Identity b))

Profunctors and pure profunctor lenses

Now guess what? The IsoFunction class is actually called Profunctor, and its methods changeInput and changeOutput are actually called lmap and rmap, and there's also a dimap method which combines lmap and rmap and which I'm going to use from now on:

lmap :: Profunctor p => (a -> b) -> p b c -> p a c
rmap :: Profunctor p => (b -> c) -> p a b -> p a c

dimap :: Profunctor p => (a -> b) -> (c -> d) -> p b c -> p a d

The point of profunctors is that if you're given a p a b, you can treat it as an opaque “black box”, some kind of relationship between a and b – you can add a filter to the black box which would modify its output, and you can add another filter which would modify its input, but you can't modify the black box itself in any way and you can't inspect the input in any way (because, after all, there might not even be any) or get any information from one filter to another (this bit might not be clear, but it'll be clear when I explain Choice in the next post).

If you want examples of profunctors in the wild, this 24 Days Of Hackage post and this School Of Haskell post give some – but (as with pretty much all abstractions!) what's useful about profunctors isn't that you can use dimap to operate on something that happens to be a profunctor, but that you can write functions which work on several profunctors. For instance, this is what lets functions in lens operate on both ordinary and indexed traversals— okay, okay, maybe there'll be indexed traversals in the post after the next post.

You could've noticed a bit of asymmetry in the definition of from – we used a custom type (Tagged a b) as a function which ignores its input, but we used a -> Const a b as a function that remembers its input and doesn't do anything else. Isn't there some custom type for that too?

How might such a type look?

newtype Input a b = ...

Okay, maybe just take a -> Const a b itself?

newtype Input a b = Input (a -> Const a b)

No, it won't work – when we go from Input a b to Input s b, the inner type will change to s -> Const s b, but we want it to be s -> Const a b. In other words, we want to create a black box which would store its immediate input and not the input of the s -> a filter that would be put in front of the black box.

The next attempt is to have an existential type – something like this:

newtype Input a b = Input (exists x. a -> x)

This way we choose x, and nobody else can do anything else with x since they don't know what x actually stands for.

Unfortunately, there's no exists in Haskell, and we can't fake it with forall like this

newtype Input a b = forall x. Input (a -> x)

because once we embed anything into Input, we'll lose information about what x was and we won't be able to get it back. So, we can resort to a simpler trick – making x a separate parameter:

newtype Input x a b = Input (a -> x)

Or, if we stick to standard terminology, the Forget type:

newtype Forget r a b = Forget { runForget :: a -> r }

instance Profunctor (Forget r) where
  dimap f _ (Forget k) = Forget (k . f)

Now we can replace Const with Forget id:

from :: Iso s t a b -> Iso b a t s
from i = iso bt sa
    sa s = runForget (i (Forget id :: Forget a a (Identity b))) s
    bt b = runIdentity . unTagged $ i (Tagged (Identity b))

(I had to give Forget id a type – involving Identity – because otherwise GHC doesn't know what functor to use and gives an “ambiguous type” error.)

And now, enter the pure profunctor lenses territory: if we were able to use Identity in both cases, we don't really need a functor there at all!

type Iso s t a b = forall p. Profunctor p => p a b -> p s t

Everything suddenly becomes much simpler when cruft stripped away:

iso :: (s -> a) -> (b -> t) -> Iso s t a b
iso = dimap

from :: Iso s t a b -> Iso b a t s
from i = iso bt sa
    sa s = runForget (i (Forget id)) s
    bt b = unTagged (i (Tagged b))

Of course, this was a pure profunctor iso, not a pure profunctor lens, and I don't want to touch pure profunctor lenses yet because I'm afraid of them:

lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
lens f g = dimap (f &&& id) (uncurry $ flip g) . first'

Maybe later.

Another derivation of Iso

I wrote this section before learning about Tagged, and I think it's interesting if only because it's another path getting to the same solution – “well, even if I never thought up that approach, I still wouldn't be entirely lost”.

Since an iso is defined by a pair of functions – s -> a and b -> t – we could try to hide those functions somewhere in the arrow. It's pretty much the same trick we used in the section about isos-as-bidirectional-functions – create a class, make functions an instance, make <something else> an instance:

  • if we end up doing lens-y things with the iso, well, we won't need <something else> at all

  • if we end up doing something iso-y, we'll demand <something else> from the iso (or the composition of isos)

The simplest thing I can think of is this:

data Hide x a b = Hide x

(a and b are phantom parameters – we need them to unify with the -> type.)

Next we can make a class:

class Hiding x p where
  hide :: x -> (a -> b) -> p a b

We can't hide anything in ->, so we just leave it intact:

instance Hiding x (->) where
  hide _ f = f

We can hide things in Hide, tho:

instance Hiding x (Hide x) where
  hide x _ = Hide x

(It's important to realise that neither Hide nor -> contain both the function and <whatever we're hiding> – it's one or the other. The trick is that since the caller gets to choose which one it wants, in reality it's both.)

Finally, that's our new type for Iso:

type Iso s t a b =
  forall p f. (Hiding (s -> a, b -> t) p, Functor f) =>
  (a -> f b) -> p s (f t)

We naively store the representation of the iso in the returned value:

iso :: (s -> a) -> (b -> t) -> Iso s t a b
iso sa bt afb = hide (sa, bt) (\s -> bt <$> afb (sa s))

And we can get it back by passing a dummy a -> f b. How can you get one? For instance, you could just always return Const () as f b, but let's use Proxy from Data.Proxy instead because learning about new things is nice and because it's sort of standard. Here's the definition of Proxy:

data Proxy t = Proxy

(It's often used to “pass a type” to a class method without having to write something like undefined :: Int – you can write Proxy :: Proxy Int instead.)

So, to reverse an iso, we extract a pair of functions from it, swap them around, and create a new iso:

from i = iso bt sa
  where Hide (sa, bt) = i (const Proxy)

Let's create a couple isos, too:

bool_int :: Iso Bool Bool Int Int
bool_int = iso fromEnum toEnum

int_char :: Iso Int Int Char Char
int_char = iso toEnum fromEnum

What's wrong with this approach? Well, nothing, apart from the fact that now our isos don't compose at all:

bool_char :: Iso Bool Bool Char Char
bool_char = bool_int.int_char
    Could not deduce (Hiding (Bool -> Int, Int -> Bool) p) …
      arising from a use of ‘bool_int’
    from the context (Hiding (Bool -> Char, Char -> Bool) p, Functor f)
      bound by the type signature for
        bool_char :: (Hiding (Bool -> Char, Char -> Bool) p, Functor f) =>
                     (Char -> f Char) -> p Bool (f Bool)
    In the first argument of ‘(.)’, namely ‘bool_int’
    In the expression: bool_int . int_char
    In an equation for ‘bool_char’: bool_char = bool_int . int_char

It's not that easy to understand the error message, so I'll just explain what went wrong:

  • We are using iso to put iso's representation into, well, the iso itself.

  • We use iso twice, so there are 4 functions that end up embedded into our 2 isos. Those functions are of types Bool -> Int, Int -> Bool, Int -> Char, and Char -> Int.

  • The composition of our isos should have the type Iso Bool Bool Char Char. It means that it should have functions of type Bool -> Char and Char -> Bool embedded into it.

  • At no point we are actually doing anything with functions of such types. There's no way they could appear!

In other words, we can't hold on indefinitely to once-embedded s -> a and b -> t, because they need to be updated when we're combining things – but currently our isos don't ever update anything.

Let's take 2 isos: isoSTAB and isoABXY. (Letters are nicer to work with than Int and Bool and Char.)

When you compose them, you would get isoSTXY (just like with lenses).

isoABXY can return:

  • a -> f b
  • Hide (a -> x, y -> b)

isoSTAB would have to turn:

  • a -> f b into s -> f t
  • Hide (a -> x, y -> b) into Hide (s -> x, y -> t) (because we need to get isoSTXY in the end)

So, we need to unify this:

(a -> f b)            -> (s -> f t)
Hide (a -> x, y -> b) -> Hide (s -> x, y -> t)

First of all, we can give Hide a different type to make unification easier:

data Hide x y a b = Hide (a -> x) (y -> b)  -- same as Hide (a -> x, y -> b)

Now it looks like this:

(a -> f b)    ->  (s -> f t)
Hide x y a b  ->  Hide x y s t

Or like this, if we make -> prefix:

(->) a (f b)  ->  (->) s (f t)
Hide x y a b  ->  Hide x y s t

Now we have f getting in the way, so let's add it to Hide. It won't break anything, because if f is a functor we would be able to choose (and it is), we can always go like this:

  • Hide x y a (f b)
  • Hide x y a (Identity b)
  • Hide x y a b

So, what we have now is this:

(->) a (f b)      ->  (->) s (f t)
Hide x y a (f b)  ->  Hide x y s (f t)


(->)       a (f b)  ->  (->)       s (f t)
(Hide x y) a (f b)  ->  (Hide x y) s (f t)

[rubs hands]


Next, let's think what we want to do with both these things— no, wait, we already know the answer:

isoSTAB would have to turn:

  • a -> f b into s -> f t
  • Hide (a -> x, y -> b) into Hide (s -> x, y -> t) (because we need to get isoSTXY in the end)

Except that with our new types...

data Hide x y a b = Hide (a -> x) (y -> b)'s a bit different. isoSTAB would have to turn:

  • a -> f b into s -> f t
  • Hide x y a (f b) into Hide x y s (f t)

Okay, at this point we have all pieces of the puzzle. We know exactly which 2 types iso has to be, and all that is left is writing a typeclass:

class MakeIso p where
  iso :: Functor f => (s -> a) -> (b -> t) -> p a (f b) -> p s (f t)

(Exercise: write instances by yourself!)

The instance for -> is straightforward:

instance MakeIso (->) where
  iso sa bt afb = \s -> bt <$> afb (sa s)

I don't like how cryptic it looks, but since writing it amounts to Just Following The Types, if you don't understand it just write it by yourself.

The instance for Hide, too, is straightforward (and <same advice applies>):

instance MakeIso (Hide x y) where
  iso sa bt (Hide ax yfb) = Hide sx yft
    where sx  = \s -> ax $ sa s
          yft = \y -> bt <$> yfb y

For the sake of completeness, here's the new Iso type:

type Iso s t a b =
  forall p f. (MakeIso p, Functor f) =>
  p a (f b) -> p s (f t)

We also need to write from, and to do that, we need to learn how to extract functions from an iso. Let's use reasoning to do it! (I mean, more reasoning reasoning than usual. Kinda. Whatever.)

We start with an Iso:

i :: Iso s t a b

Expand it using the definition of Iso:

type Iso s t a b =
  forall p f. (MakeIso p, Functor f) =>
  p a (f b) -> p s (f t)

i :: (MakeIso p, Functor f) => p a (f b) -> p s (f t)

(I removed forall p f because it's not really needed there.)

Now, we actually know what we want to get – Hide a b s (f t):

p s (f t) = Hide a b s (f t)

From it we can deduce:

p = Hide a b

And therefore:

i :: (Functor f) => Hide a b a (f b) -> Hide a b s (f t)

Also, since we can choose f, let's choose Identity because we can always strip it away:

i :: Hide a b a (Identity b) -> Hide a b s (Identity t)

We're going to get our Hide a b s (Identity t) if we can get Hide a b a (Identity b) from somewhere. Can we get it?

data Hide a b a (Identity b) = Hide (a -> a) (b -> Identity b)

Yep, seems easy enough – Hide id Identity. And once we have s -> Identity t, we can combine it with runIdentity to get s -> t:

from i = iso (runIdentity . bt) sa
  where Hide sa bt = i (Hide id Identity)

It's only left to notice that MakeIso is pretty much just like Profunctor:

class MakeIso p where
  iso :: Functor f => (s -> a) -> (b -> t) -> p a (f b) -> p s (f t)

class Profunctor p where
  dimap :: (a -> b) -> (c -> d) -> p b c -> p a d

Hide can be easily made an instance:

instance Profunctor (Hide x y) where
  dimap sa bt (Hide ax yb) = Hide sx yt
    where sx = ax . sa
          yt = bt . yb

Then we can change the definitions of Iso and iso:

type Iso s t a b =
  forall p f. (Profunctor p, Functor f) =>
  p a (f b) -> p s (f t)

iso :: (s -> a) -> (b -> t) -> Iso s t a b
iso sa bt = dimap sa (fmap bt) afb

Rename Hide to Exchange (because that's how it's called in lens):

data Exchange a b s t = Exchange (s -> a) (b -> t)

Write an explicit type – AnIso – for <whatever from accepts>:

type AnIso s t a b = Exchange a b a (Identity b) ->
                     Exchange a b s (Identity t)

This type can be used whenever we want to write a function which takes an Iso, because it's the “smallest” type that fully describes an iso. (By the way, Lens and Traversal have similar types associated with them – ALens and ATraversal – but they're implemented using weird-sounding things like Bazaar and Pretext with very helpful descriptions like “a.k.a. indexed Cartesian store comonad, indexed Kleene store comonad, or an indexed FunList” and I don't want to touch them with a 10-foot pole. [sighs] I guess I'll have to sooner or later...)

This approach has actually led us slightly further than the previous one – previously we needed -> and Tagged to implement from, but now we can extract both the “getter” and the “setter” in 1 pass (using Exchange), and we also can write AnIso (which we couldn't do before because we needed the iso to be polymorphic to use it with both -> and Tagged). However, we definitely could've invented Exchange without all the mess in this section, by noticing that:

  • We can try to combine Forget and Tagged to get both thing we need in 1 pass:

    data FT r a b = FT (a -> r) b

    and make it an instance of Profunctor and everything.

  • We can't, however, use FT to get both s -> a and b -> t, because currently we get b -> t basically by feeding b to the iso and taking t from the result. So, with FT we can have this:

    bsat :: b -> (s -> a, t)
    bsat b = let ... = i (FT id (Identity b)) in ...

    but we can't get b -> t and s -> a separately.

  • The trick is in using exists x. x -> b instead of b as our type for “constant function” – they're equivalent:

    data FT r x a b = FT (a -> r) (x -> b)

    (and again we replace exists with the type-variable-outside trick).

  • And now FT is actually Exchange with a different name and type variables. Ha.

Some useful isos

strict converts a lazy Text or ByteString to a strict one (and back):

> lazyTexts & each.strict %~ doSomethingWithStrictText

(You can use from strict to go in the opposite direction, or you can use lazy.)

reversed is an isomorphism between things and... well, reversed things:

> "live" ^. reversed

> "live" & reversed %~ ('d':)

(It has instances for many various containers, as well as Text and so on.)

swapped swaps/unswaps sides of tuples or Either:

> (1,2) ^. swapped

> Left "hi" ^. swapped
Right "hi"

non is an isomorphism for Maybe a that lets you assign some arbitrary value to Nothing:

non :: Eq a => a -> Iso' (Maybe a) a
> Just 185 ^. non 0

> Nothing ^. non 0

It's more useful, however, when you're using it for setting/modifying – it combines the “modifying” step and the “checking we don't have any values we don't like” step:

> Just 185 & non 0 -~ 185

It's even more useful when you're working with maps and at and also nested maps and stuff, but I haven't talked about at yet and I don't want to do it now, so just read the examples in the docs for non if you're interested.