HiveBrain v1.2.0
Get Started
← Back to all entries
patternModerate

Capture the notion of invertible functions

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
notionthecaptureinvertiblefunctions

Problem

I find sometimes it is useful to capture the notion of invertible functions.

The idea is that if two functions f :: a -> b and g :: b -> a are the inverse function of each other, and if there is another function h :: b -> b, then h can also work on values of type a.

Moreover, if there f' and g' are another pair of functions that are the inverse function of each other, (f,g) and (f',g') can actually be composed to (f' . f, g . g') and the invertibility still holds.

The following is my attempt to implement this in Haskell, and I'm wondering if an existing library can do the same thing (or even more general thing) for me.
Also advice and comments about my code are appreciated.

Implementation

First I use records to store two functions:

data Invertible a b = Invertible
    { into :: a -> b
    , back :: b -> a
    }


into means "convert a into b" while back means "convert b back to a".

And then few helper functions:

selfInv :: (a -> a) -> Invertible a a
selfInv f = Invertible f f

flipInv :: Invertible a b -> Invertible b a
flipInv (Invertible f g) = Invertible g f

borrow :: Invertible a b -> (b -> b) -> a -> a
borrow (Invertible fIn fOut) g = fOut . g . fIn

liftInv :: (Functor f) => Invertible a b -> Invertible (f a) (f b)
liftInv (Invertible a b) = Invertible (fmap a) (fmap b)


In the above code borrow will use the pair of functions to make its last argument g
available to values of type a. And changing borrow f to borrow (flipInv f) will make g available to values of type b. Therefore borrow captures my initial idea of making a function of type b -> b available for values of a if a and b can be converted to each other.

In addition, Invertible forms a monoid-like structure, I use rappend and rempty to suggest a similiarity between it and Monoid:

```
rempty :: Invertible a a
rempty = selfInv id

rappend :: Invertible a b
-> Invertible b c
-> Invertible a c
(Invertible f

Solution

Naming:

There are a lot of names I'd change. Some of them matter more than others (local names in a let are not that big a deal even if they're terrible, but a name for a widely used function should be suggestive, if possible). Some of them I'm more sure of than others.

I'd change borrow to within. The old name isn't bad, but I think the new one is suggestive in the right way. I'd change Invertible to Transform, since an invertible function is just the one function, and we're talking about a pair of related functions. Bijection is the mathematical name for it, and it would make a good name too, but it seems (to me at least) to suggest that the domain and range are the entire input and output types, and not every function we'd want to use with this fits that.

I'd change the local invertible to transform to match, so borrow invertible (map compactLeft) becomes within transform (map compactLeft).

I'd change mirrorI -> mirror and diagonalI -> diagonal; the I doesn't add anything, it just looks as if you're using roman numerals at first. I'd also change DD, DL, DR, DU -> DOWN, LEFT, RIGHT, UP, as it's just clearer.

The Inv suffix doesn't help. Not all of the functions have it, and it isn't spelled out enough to be suggestive. Also, if we change it from Invertible, it no longer makes sense. So, selfInv -> involution, liftInv -> lift, flipInv -> backwards. An involution is the mathematical name for a function that is its own inverse (and if you didn't know that, don't feel bad, I was only guessing there might be a name for it until I googled it). flip is already taken, and I think backwards is more suggestive (reverse would have been nice too, but it's also taken). lift isn't taken as far as I know, but it's the sort of thing that seems like it might be. If so, liftT or liftTr might work (or liftB if we're using Bijection).

I'd change fIn, fOut -> f, f'. Using f' doesn't quite say "I'm an inverse", but it suggests it somewhat if we know how Transform is defined/meant to be used.

I can see why you named rempty and rappend as you did, but the names made me (and would probably make other people) want to try to make it a Monoid instance, and that doesn't work. I'd change rempty -> idT, rappend -> >>>. >>> matches the Category class, and is nicely intuitive (and doesn't need backquotes). I'm not really sure about idT, and it could probably be improved, but it still seems a bit nicer than rempty to me.

I'd also change into, back -> fwd, rev, but I don't have much justification here. It seems a little more suggestive of a thing that can go both ways, instead of a box that you can go into and come out of, but can't be flipped inside out. The original names here are good.

All Namechanges in One Spot:

DD, DL, DR, DU -> DOWN, LEFT, RIGHT, UP

Invertible -> Transform

invertible -> transform

mirrorI -> mirror

diagonalI -> diagonal

selfInv -> involution

liftInv -> lift

flipInv -> backwards

into, back -> fwd, rev

fIn, fOut -> f, f'

rempty -> idT

rappend -> >>>

Instances that don't work:

Monad, Applicative, and Functor don't work because fmap :: (a -> b) -> f a -> f b and that doesn't fit.

Monoid doesn't work because mappend :: a -> a -> a, and our composition takes 2 different types and returns yet another different type.

Arrow seems like it should work at first, but arr :: (b -> c) -> a b c. The types match nicely, but we'd need a way that makes sense to take an arbitrary function and find its inverse. Functions and Arrows go one way, but we want to go both ways.

Category fits perfectly, but when I tried to get it to work, the compiler complained about all the uses of . in the program being ambiguous. There may be a way to make it work, but looking at Control.Category, the gains were almost nonexistent, so I abandoned the attempt. The interface (>>> and <<< for forward and backward composition), was nice, though, so I kept it.

Instances that do work:

None?

Non-Naming Changes:

The suggestion in the comments by mjolka is correct, and the resulting code:

modShift base offset = Transform f g
    where
        f x = (x + offset) `mod` base
        g y = (y - offset) `mod` base


looks nicer and is clearer.

This:

compactLeft xs = nonZeros ++ replicate (((-) `on` length) xs nonZeros) 0
    where nonZeros = filter (/= 0) xs


is a bit ugly, and can be simplified to this:

compactLeft xs = take (length xs) $ nonZeros ++ repeat 0
    where nonZeros = filter (/= 0) xs


I'd introduce a new function that abstracts the pattern in the parentheses in

caesarCipher = liftInv (upperAsOrd
                        -- Char  Int
                        `rappend` caesarShift
                        -- Int  Int
                        `rappend` flipInv upperAsOrd)
                        -- Int  Char


making my suggested namechanges, it becomes:

```
caesar

Code Snippets

modShift base offset = Transform f g
    where
        f x = (x + offset) `mod` base
        g y = (y - offset) `mod` base
compactLeft xs = nonZeros ++ replicate (((-) `on` length) xs nonZeros) 0
    where nonZeros = filter (/= 0) xs
compactLeft xs = take (length xs) $ nonZeros ++ repeat 0
    where nonZeros = filter (/= 0) xs
caesarCipher = liftInv (upperAsOrd
                        -- Char <-> Int
                        `rappend` caesarShift
                        -- Int <-> Int
                        `rappend` flipInv upperAsOrd)
                        -- Int <-> Char
caesarCipher = lift (upperAsOrd >>> caesarShift >>> backwards upperAsOrd)

Context

StackExchange Code Review Q#44550, answer score: 10

Revisions (0)

No revisions yet.