14

Let's say I have a higher order function f :: (a -> b) -> (a -> b). But f only behaves properly if the input function is surjective. Is there anyway to force this to happen in Haskell? For example, I really want f's type signature to be something like:

f :: (Surjective (a -> b)) => (a -> b) -> (a -> b)

But this doesn't work because I don't want all functions of the type a -> b to be declared to be surjective, only some of them. For example, maybe f converts a surjective function into a non-surjective function.

We could wrap the functions in a special data type data Surjective f = Surjective f, and define

f :: Surjective (a -> b) -> (a -> b)

but this would make it difficult to assign multiple properties to a function.

Is there any convenient way to do this in practice? Is this even possible in theory?

Matt Fenwick
  • 48,199
  • 22
  • 128
  • 192
Mike Izbicki
  • 6,286
  • 1
  • 23
  • 53
  • 2
    Haskell's type system is great, but doing this as you'd like is just a bit too much for it. This is really quite far into the realm of dependently-typed languages, so you should try Agda or Coq. However, I consider the `newtype` approach (or typeclass for newtypes) quite ok, though it would be up to you to proove surjectivity. – leftaroundabout Nov 08 '12 at 14:07

3 Answers3

6

This is an example of how surjectivity can be expressed in Agda:

module Surjectivity where

open import Data.Product using ( ∃; ,_ )
open import Relation.Binary.PropositionalEquality using ( _≡_; refl )

Surjective : ∀ {a b} {A : Set a} {B : Set b} → (A → B) → Set _
Surjective {A = A} {B = B} f = ∀ (y : B) → ∃ λ (x : A) → f x ≡ y

For example, id is surjective (a simple proof):

open import Function using ( id )

id-is-surjective : ∀ {a} {A : Set a} → Surjective {A = A} id
id-is-surjective _ = , refl

Taking another identity function which works only for surjective functions:

id-for-surjective's : ∀ {a b} {A : Set a} {B : Set b} → (F : A → B) → {proof : Surjective F} → A → B
id-for-surjective's f = f

we can pass id to id-for-surjective's with its surjectivity proof as witness:

id′ : ∀ {a} {A : Set a} → A → A
id′ = id-for-surjective's id {proof = id-is-surjective}

so that id′ is the same function as id:

id≡id′ : ∀ {a} {A : Set a} → id {A = A} ≡ id′
id≡id′ = refl

Trying to pass a non-surjective function to id-for-surjective's would be impossible:

open import Data.Nat using ( ℕ )

f : ℕ → ℕ
f x = 1

f′ : ℕ → ℕ
f′ = id-for-surjective's f {proof = {!!}} -- (y : ℕ) → ∃ (λ x → f x ≡ y) (unprovable)

Similary, many other properties can be expressed in such manner, Agda's standard library already have necessary definitions (e.g. Function.Surjection, Function.Injection, Function.Bijection and other modules).

JJJ
  • 2,731
  • 1
  • 12
  • 23
  • Another reason in my list of Why I must get around to learning Agda someday. Thanks. Is this part of the reason Agda runs slower than Haskell? – Mike Izbicki Nov 22 '12 at 04:01
  • @MikeIzbicki, not really, in dependently typed languages all the proofs performed by the type checker (i.e. the type checker answers the question "whether this program-as-proof proves its type-as-proposition (= well-typed) or not (= ill-typed)?"), so that everything interesting happens at compile time in the type system. Run-time programs obtained by types/proofs erasure and code extraction can be fast or not, it depends on the implementation. For example, MAlonzo backend which compiles Agda to Haskell adds a certain overhead, but I see no good reason why this should necessarily be so. – JJJ Nov 23 '12 at 06:38
  • @MikeIzbicki see also [this](http://stackoverflow.com/a/13241158/1337941) answer. – JJJ Nov 23 '12 at 06:39
4

We can kind of simlutate dependent types in Haskell if the type is dependent on a value which is uniquely determined by its type. Well that's not dependent types, of course, but it can be useful sometimes.

So let's build a kind of small constructive set theory at the type level. Each type will represent a particular function, and will be inhabited by a single value (excluding all the bottom thing).

Define F as the smallest set satisfying the following:

  • id:: a -> a is in F.
  • term:: a -> () is in F.
  • init:: Empty -> a is in F (where Empty represents the empty set).
  • p1 :: (a,b) -> a is in F.
  • i1 :: a -> Either a b is in F.
  • flip :: (a,b) -> (b,a) is in F.
  • if both f::a -> b and g::b -> c are in F, then g.f :: a -> c is in F.
  • if both f::a -> b and g::c -> d are in F, then the following functions are in F:

      f*g :: (a,c) -> (b,d)
      f*g (x,y) = (f x,g y)
      f + g :: Either a b -> Either c d
      (f+g) (Left x) = f x
      (f+g) (Right y) = g y`
  • (Add ohter inductive rules here so that your favorite functions can be included in F.)

The set F is meant to represent the functions that are encodable at the type level in Haskell, and at the same time whose various properties such as surjectivity, injectivity, etc. are provable by the type-level functions in Haskell.

With the help of associative types, we can encode F cleanly as follows:

class Function f where
    type Dom f :: *
    type Codom f :: *
    apply :: f -> Dom f -> Codom f

data ID a = ID  -- represents id :: a -> a
instance Function (ID a) where
    type Dom (ID a) = a
    type Codom (ID a) = a
    apply _ x = x

data P1 a b = P1 -- represents the projection (a,b) -> a
instance Function (P1 a b) where
    type Dom (P1 a b) = (a,b)
    type Codom (P1 a b) = a
    apply _ (x,y) = x

...

data f :.: g = f :.: g  -- represents the composition (f.g)
instance ( Function f
         , Function g
         , Dom f ~ Codom g)
         => Function (f :.: g) where
     type Dom (f :.: g) = Dom g
     type Codom (f :.: g) = Codom f
     apply (f :.: g) x = apply f (apply g x)
 ...

The type-level predicate "f is surjective" can be expressed as class instances:

class Surjective f where
instance Surjective (ID a)  where
instance Surjective (P1 a b)  where
instance (Surjective f,Surjective g)
     => Surjection (f :.: g) where
 ..

Finally a higher order function that takes those provably surjective functions can be defined:

surjTrans :: (Function fun,Surjective fun)
             => fun -> Dom fun -> Codom fun
surjTrans surj x = apply surj x

Similarly for injections, isomorphisms, etc. For example, a higher order function that only takes (constructive) isomorphisms as arguments can be declared:

isoTrans :: (Function fun,Surjective fun,Injective fun)
            => fun -> Dom fun -> Codom fun
isoTrans iso x = apply iso x

If the transformations take a more interesting form, then it will have to be a class method and defined by a structural recursion for each function (which is uniquely determined by its type).

I'm certainly no expert in logic or Haskell, and I would really like to know how powerful this theory can be. If you found this, could you post an update?

Here is the full code:


{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}

infixl 6 :.:
infixl 5 :*:
infixl 4 :+:

data TRUE
data Empty

class Function f where
  type Dom f :: *
  type Codom f :: *
  apply :: f -> Dom f -> Codom f

instance Function (a -> b) where
  type Dom (a->b) = a
  type Codom (a->b) = b
  apply f x = f x

data ID a = ID
data Initial  a = Initial
data Terminal a = Terminal
data P1 a b = P1
data P2 a b = P2
data I1 a b = I1
data I2 a b = I2
data FLIP a b = FLIP
data COFLIP a b = COFLIP
data f :.: g = f :.: g
data f :*: g = f :*: g
data f :+: g = f :+: g

instance Function (ID a) where
  type Dom (ID a) = a
  type Codom (ID a) = a
  apply _ x = x

instance Function (Initial a) where
  type Dom (Initial a) = Empty
  type Codom (Initial a) = a
  apply _ _ = undefined

instance Function (Terminal a) where
  type Dom (Terminal a) = a
  type Codom (Terminal a) = ()
  apply _ _ = ()

instance Function (P1 a b) where
  type Dom (P1 a b) = (a,b)
  type Codom (P1 a b) = a
  apply _ (x,y) = x

instance Function (P2 a b) where
  type Dom (P2 a b) = (a,b)
  type Codom (P2 a b) = b
  apply _ (x,y) = y

instance Function (I1 a b) where
  type Dom (I1 a b) = a
  type Codom (I1 a b) = Either a b
  apply _ x = Left x

instance Function (I2 a b) where
  type Dom (I2 a b) = b
  type Codom (I2 a b) = Either a b
  apply _ y = Right y

instance Function (FLIP a b) where
  type Dom (FLIP a b) = (a,b)
  type Codom (FLIP a b) = (b,a)
  apply _ (x,y) = (y,x)

instance Function (COFLIP a b)  where
  type Dom (COFLIP a b) = Either a b
  type Codom (COFLIP a b) = Either b a
  apply _ (Left x) = Right x
  apply _ (Right y) = Left y

instance ( Function f
         , Function g
         , Dom f ~ Codom g)
         => Function (f :.: g) where
  type Dom (f :.: g) = Dom g
  type Codom (f :.: g) = Codom f
  apply (f :.: g) x = apply f (apply g x)

instance (Function f, Function g)
         => Function (f :*: g)  where
  type Dom (f :*: g) = (Dom f,Dom g)
  type Codom (f :*: g) = (Codom f,Codom g)
  apply (f :*: g) (x,y) = (apply f x,apply g y)

instance (Function f, Function g)
         => Function (f :+: g) where
  type Dom (f :+: g) = Either (Dom f) (Dom g)
  type Codom (f :+: g) = Either (Codom f) (Codom g)
  apply (f :+: g) (Left x)  = Left (apply f x)
  apply (f :+: g) (Right y) = Right (apply g y)



class Surjective f where
class Injective f  where
class Isomorphism f where

instance Surjective (ID a)  where
instance Surjective (Terminal a)  where
instance Surjective (P1 a b)  where
instance Surjective (P2 a b)  where
instance Surjective (FLIP a b)  where
instance Surjective (COFLIP a b) where
instance (Surjective f,Surjective g)
         => Surjective (f :.: g) where
instance (Surjective f ,Surjective g )
         => Surjective (f :*: g)  where
instance (Surjective f,Surjective g )
         => Surjective (f :+: g)  where

instance Injective (ID a)  where
instance Injective (Initial a)  where
instance Injective (I1 a b)  where
instance Injective (I2 a b)  where
instance Injective (FLIP a b)  where
instance Injective (COFLIP a b)  where
instance (Injective f,Injective g)
         => Injective (f :.: g) where
instance (Injective f ,Injective g )
         => Injective (f :*: g)  where
instance (Injective f,Injective g )
         => Injective (f :+: g)  where

instance (Surjective f,Injective f)
         => Isomorphism f  where


surjTrans :: (Function fun,Surjective fun)
             => fun -> Dom fun -> Codom fun
surjTrans surj x = apply surj x

injTrans :: (Function fun,Injective fun)
            => fun -> Dom fun -> Codom fun
injTrans inj x = apply inj x

isoTrans :: (Function fun,Isomorphism fun)
            => fun -> Dom fun -> Codom fun
isoTrans iso x = apply iso x


g1 :: FLIP a b
g1 = FLIP

g2 :: FLIP a b :*: P1 c d
g2 = FLIP :*: P1

g3 :: FLIP a b :*: P1 c d :.: P2 e (c,d)
g3 = FLIP :*: P1 :.: P2

i1 :: I1 a b
i1 = I1

For example, here are some of the outputs (see how Haskell 'proves' those recursive properties when typechecking):


isoTrans  g1 (1,2)
==> (2,1)
surjTrans g2 ((1,2),(3,4))
==> ((2,1),3)
injTrans  g2 ((1,2),(3,4))
==>     No instance for (Injective (P1 c0 d0)) ..

surjTrans i1 1 :: Either Int Int
==>     No instance for (Surjective (I1 Int Int)) ..
injTrans i1 1 :: Either Int Int
==>  Left 1
isoTrans i1 1 :: Either Int Int
==>      No instance for (Surjective (I1 Int Int)) ..
mnish
  • 1,869
  • 1
  • 13
  • 15
  • I don't fully understand this answer, but +1 for very interesting results. :-) – Jamey Sharp Nov 10 '12 at 19:17
  • Very interesting. This seems to have the power of what I was thinking of, and seems much cleaner to me than the `newtype` method. My intuition is that any function can be converted into the form you describe. If that's the case, it seems like it should be possible to implement some sort of language extension to do all this jazz behind the scenes and directly annotate functions. Can anyone chime in on the potential obstacles to that? – Mike Izbicki Nov 12 '12 at 19:40
  • @Mike: I'm afraid it's not easy to extend this to include many interesting 'real world' functions without breaking the soundness. On one hand surjectivity is a very subtle global property, on the other hand Haskell doesn't even allow encoding first order logic (for example disjunctions). If you have found an example of interesting surjective function that's encodable in Haskell this way, I hope you could share! – mnish Nov 13 '12 at 06:59
3

First, you'd typically use a newtype declaration rather than a data declaration. GHC uses newtypes for type-checking but then effectively erases them during compilation, so the generated code is more efficient.

Using newtype for this kind of annotation is a common solution in Haskell, although as you point out it's not entirely satisfying if you need to wrap many properties around a value.

You can combine newtype with type-classes. Declare a type-class instance for a Surjective type-class on any newtype wrapper you want and match against that type-class in functions like f, instead of requiring a specific newtype wrapper.

Even nicer, of course, would be the ability to get the compiler to check that the function truly is surjective... but that's rather an open research problem. :-)

Jamey Sharp
  • 8,363
  • 2
  • 29
  • 42
  • This solution still isn't very satisfying because I can't actually call a function inside a newtype without unwrapping it. It would be much better if the "context" was permanently associated with the function just like the case with normal classes. – Mike Izbicki Nov 08 '12 at 02:23
  • 7
    But you want it to be associated with _the function_, not the type, right? Typeclasses are properties of entire types, not of individual values. Surjectivity is a property of an individual function (value), and as such, to have a type that says so would require dependent types. The newtype that Jamey suggests using is effectively creating a "surjective function arrow": `newtype Surjective a b = Surjective (a -> b)` that is meant to remain abstract. Then you can control what lives in it and have application, composition, etc. defined on `Surjective`. I doubt you can do much better in Haskell. – copumpkin Nov 08 '12 at 05:21
  • @copumpkin It would be nice if I could use the Surjective a b type directly as a function without unwrapping it, if I could prove with the type system that the composition of two surjective functions is also surjective, etc. I don't see this being possible with the newtype method, which means maybe it isn't possible in Haskell, IDK. – Mike Izbicki Nov 08 '12 at 06:50
  • 1
    @copumpkin's comment reminds me that Category and Arrow instances for these newtypes are trivial. (Generalized newtype deriving can produce them automatically, I think?) So at least you can use arrow notation or the arrow/category combinators even if straight function call syntax is a pain. I imagine that's what copumpkin meant but it wasn't obvious to me, so I'm sharing. :-) – Jamey Sharp Nov 08 '12 at 07:02
  • 3
    @MikeIzbicki you can "prove" it that way. You can write `composeSurj : Surjective b c -> Surjective a b -> Surjective a c` which is a logical statement that the composition of surjective functions is surjective. And what @JameySharp says is true: you can write a `Category`/`Arrow` instance for `Surjective` and compose them as you might otherwise. It's not ideal, but unless you use Agda or Coq (or similar), you're not going to be talking about properties of individual values in types. – copumpkin Nov 08 '12 at 17:01