reviewN to review with multiple arguments!

Posted on 2018-12-29 by Oleg Grenrus lens

A short gist about multi-argument things. Today is about Prisms.

#Introduction

Let's consider an example type and its autogenerated Prisms.

data FooBarBaz = Foo Int | Bar Int Char | Baz
  deriving Show

$(makePrisms ''FooBarBaz)

The _Foo is a nice Prism' FooBarBaz Int. For example, we can use it to construct FooBarBaz

λ> _Foo # 42
Foo Int

But _Bar is unpleasant Prism' FooBarBaz (Int, Char). We have to tuple

λ> _Bar # (42, 'f')
Bar 42 'f'

Can we do better?

#Abstract over helper functions

We can write helpers

review2 :: AReview t (b1, b2) -> b1 -> b2 -> t
review2 l b1 b2 = review l (b1, b2)

review3 :: AReview t (b1, b2, b3) -> b1 -> b2 -> b3 -> t
review3 l b1 b2 b3 = review l (b1, b2, b3)

but those aren't satisfying.

Luckily, Prism carries "amount of arguments", so we can write a a quite simple type class to abstract over review2, review3...

class ReviewN tuple where
    type ReviewNArgs tuple t :: *
    reviewN :: AReview t tuple -> ReviewNArgs tuple t

instance ReviewN (b1, b2) where
    type ReviewNArgs (b1, b2) t = b1 -> b2 -> t
    reviewN = review2

instance ReviewN (b1, b2, b3) where
    type ReviewNArgs (b1, b2, b3) t = b1 -> b2 -> b3 -> t
    reviewN = review3

So now we can write nicer auto-uncurried code:

λ> reviewN _Bar 42 'f'
Bar 42 'f'

That's an improvement

Note that if we try to define an infix operator:

(##) :: ReviewN tuple => AReview t tuple -> ReviewNArgs tuple t
(##) = reviewN

infixr 8 ##

it won't work as we want. We'd need a fixity tighter than juxtaposition. :(

λ> (_Bar ## 42) 'f'
Bar 42 'f'

#Zero arguments

Let's not forget the nullary constructor Baz. It generates _Baz :: Prism' FooBarBaz ().

Our ReviewN can easily handle this too: () is an empty tuple:

instance ReviewN () where
    type ReviewNArgs () t = t

    reviewN = flip review ()
λ> reviewN _Baz
Baz

#Single argument

But how about the very nice _Foo. There are No instance for (ReviewN Int) arising from a use of ‘reviewN’. Do we have to switch between review and reviewN?

One solution is to use Only (or Identity or ...)

instance ReviewN (Only b) where
    type ReviewNArgs (Only b) t = b -> t
    reviewN l =  review l. Only

_Foo' :: Prism' FooBarBaz (Only Int)
_Foo' = _Foo . coerced

Using _Foo' prism, reviewN works fine too:

λ> reviewN _Foo' 42
Foo 42

#Conclusion

This is a very small example of somewhat generic programming. I think this (obvious?) trick might be useful in DSLs where "functions" of the DSL can have multiple inputs (or outputs!) - there we can choose to wrap single parameters in Only, which would allow to abstracti over arity.

Prism (and optics) happen to be quite general DSL ;)

Also I'm pretty sure that this can be made work with generic-lens machinery, so in a single argument case (Only b) is generated, so we could write:

reviewNCtor @"Foo" 42
reviewNCtor @"Bar" 42 'f'
reviewNCtor @"Baz"

Which is silly as we can just

Foo 42
Bar 42 'f'
Baz

but again, you never know where that could be useful.

Site proudly generated by Hakyll