Search code examples
haskellhaskell-lens

Confused about constructing an Iso from another Iso


I am currently writing some code to wrangle logical forms in a particular way. In pursuit of this, I have written the following code, which makes use of lenses:

{-# LANGUAGE NoImplicitPrelude #-}            
{-# LANGUAGE Rank2Types #-} 

module Logic.Internal.Formula where 

import BasicPrelude hiding (empty, negate)        
import qualified Data.Set as Set 
import Control.Lens 

data Atom = Pos { i :: Integer}                                                                                           
          | Neg { i :: Integer}                                                                                           
          deriving (Eq, Ord, Show, Read)                                                                                  

negatingAtom :: Iso' Atom Atom                                                                                            
negatingAtom = iso go go                                                                                                  
  where go (Pos x) = Neg x                                                                                                
        go (Neg x) = Pos x                                                                                                

newtype Conjunction a = Conjunction (Set a)                                                                               
  deriving (Eq, Ord, Read, Show)                                                                                          

conjuncts :: Conjunction a -> [a]                                                                                         
conjuncts (Conjunction x) = Set.toList x                                                                                  

newtype Disjunction a = Disjunction (Set a)                                                                               
  deriving (Eq, Ord, Read, Show)
disjuncts :: Disjunction a -> [a]                                                                                         
disjuncts (Disjunction x) = Set.toList x                                                                                  

negatingClause :: Iso' (Conjunction Atom) (Disjunction Atom)                                                              
negatingClause = liftClause negatingAtom                                                                                  

type CNF = Conjunction (Disjunction Atom)                                                                                 
type DNF = Disjunction (Conjunction Atom)

-- Helper stuff                                                               

liftClause :: (Ord a) => Iso' a a -> Iso' (Conjunction a) (Disjunction a)                                                 
liftClause x = let pipeline = Set.fromList . fmap (view x) in                                                             
  iso (Disjunction . pipeline . conjuncts) (Conjunction . pipeline . disjuncts)

Then, I attempted to write the following in (what I believed was) the natural way:

type CNF = Conjunction (Disjunction Atom)                                                                                 
type DNF = Disjunction (Conjunction Atom)                                                                                 

negatingForm :: Iso' CNF DNF                                                                                              
negatingForm = liftClause negatingClause

However, the typechecker was definitely not happy with this, and I'm not at all sure why. The exact output is below:

Couldn't match type ‘Conjunction Atom’ with ‘Disjunction Atom’                                                            
Expected type: p1 (Disjunction Atom) (f1 (Disjunction Atom))                                                              
               -> p1 (Disjunction Atom) (f1 (Disjunction Atom))                                                           
  Actual type: p1 (Disjunction Atom) (f1 (Disjunction Atom))                                                              
               -> p1 (Conjunction Atom) (f1 (Conjunction Atom))                                                           
In the first argument of ‘liftClause’, namely ‘negatingClause’                                                            
In the expression: liftClause negatingClause

I'm a bit new to lenses, and would really like to understand what I misunderstood here, as well as how I would implement the required Iso.


Solution

  • (Answer also posted to http://lpaste.net/164691)

    Here is a solution using TypeFamilies.

    First we'll simplify the set up:

     {-# LANGUAGE TypeFamilies #-}
    
     import Control.Lens
    
     data Atom = Pos Int | Neg Int deriving (Show)
     newtype And a = And [a]       deriving (Show)
     newtype Or a  = Or [a]        deriving (Show)
    

    The goal is create the following sequence of functions:

    f0 :: Atom -> Atom                      g0 :: Atom -> Atom
    f1 :: And Atom -> Or Atom               g1 :: Or Atom -> And Atom
    f2 :: And (Or Atom) -> Or (And Atom)    g1 :: Or (And Atom) -> And (Or Atom)
    ...
    

    The f's and g's are inverses of each other. From these functions we can create the Iso' lenses:

    iso0 = iso f0 g0  :: Iso' Atom Atom
    iso1 = iso f1 g1  :: Iso' (And Atom) (Or Atom)
    iso2 = iso f2 g2  :: Iso' (And (Or Atom)) (Or (And Atom))
    

    We start by creating the type family Negated which acts as a type function. Negated a is the type that an f-function (or g-function) maps a to.

     type family Negated a
    
     type instance Negated Atom = Atom
     type instance Negated (And a) = Or (Negated a)
     type instance Negated (Or a) = And (Negated a)
    

    For instance, Negated (And (Or Atom)) is the type Or (And Atom)).

    Next, we define a type class to perform the inverting operation:

     class Invertible a where
       invert :: a -> Negated a
    
     instance Invertible Atom where
       invert (Pos a) = Neg a
       invert (Neg a) = Pos a
    
     instance Invertible a => Invertible (And a) where
       invert (And clauses) =  Or (map invert clauses)
    
     instance Invertible a => Invertible (Or a) where
       invert (Or clauses) = And (map invert clauses)
    

    The definitions of the isomorphisms are now trivial:

     iso0 :: Iso' Atom Atom
     iso0 = iso invert invert
    
     iso1  :: Iso' (And Atom) (Or Atom)
     iso1 = iso invert invert
    
     iso2 :: Iso' (And (Or Atom)) (Or (And Atom))
     iso2 = iso invert invert
    

    Examples:

     and1 = And [ Pos 1, Neg 2, Pos 3 ]
     or1 = Or [and1]
    
     test1 = invert and1                 -- Or [Neg 1,Pos 2,Neg 3]
     test2 = invert or1                  -- And [Or [Neg 1,Pos 2,Neg 3]]
    

    Note that one limitation of this approach to modeling logical predicates is that all the Atoms must be at the same depth in the expression tree. E.g., you can't represent this tree (here x, y and z are Atoms):

                 and
                /   \
               x    or
                   /  \
                  y    z