Etherian
Etherian

Reputation: 119

How to define a pair type in Idris that only holds certain combinations of values

I'm trying to learn about dependent types in Idris by attempting something way out of my depth. Please bear with me if I make any silly mistakes.

Given the simple type

data Letter = A | B | C | D

I would like to define a type LPair that holds a pair of Letter such that only "neighboring" letters can be paired. For example, B can be paired with A or C, but not D or itself. It wraps around, so A and D are treated as neighbors.

In practice, given x : Letter and y : Letter, mklpair x y would be equivalent to mklpair y x, but I'm not sure if that property can or should be reflected in the type.

What is the best way to go about making such a type?

Upvotes: 3

Views: 277

Answers (2)

imuli
imuli

Reputation: 186

What you need is a proof that the letters are neighbouring. So with your definition of neighbours:

data Letter = A | B | C | D

neighbours : Letter -> Letter -> Bool
neighbours A B = True
neighbours B A = True
neighbours B C = True
neighbours C B = True
neighbours C D = True
neighbours D A = True
neighbours A D = True
neighbours _ _ = False

data LPair : Type where
  MkLPair : (x : Letter) -> (y : Letter) -> {auto prf : neighbours x y = True} -> LPair

The {} make the prf argument implicit, while the auto tells Idris to figure it out.

Upvotes: 4

xash
xash

Reputation: 3722

The most straightforward way is to create a subset of (Letter, Letter) which elements fulfill the predicate (a, b : Letter) -> Either (Next a b) (Next b a), where Next is are just the neighbors to the right:

data Letter = A | B | C | D

data Next : Letter -> Letter -> Type where
  AB : Next A B
  BC : Next B C
  CD : Next C D
  DA : Next D A

Neighbour : Letter -> Letter -> Type
Neighbour a b = Either (Next a b) (Next b a)

LPair : Type
LPair = (p : (Letter, Letter) ** Neighbour (fst p) (snd p))

mklpair : (a : Letter) -> (b : Letter) -> {auto prf : Neighbour a b} -> LPair
mklpair a b {prf} = ((a, b) ** prf)

N.B.: This approach is nice and simple, but once you implement a function that decides if a and b are next to each other, you'll need around (number of letters)^3 cases:

isNext : (a : Letter) -> (b : Letter) -> Dec (Next a b)
isNext A A = No nAA where
  nAA : Next A A -> Void
  nAA AB impossible
  nAA BC impossible
  nAA CD impossible
  nAA DA impossible
isNext A B = Yes AB
...

If you have a lot of letters and need a decision function, the usual approach is thus to map your data to a recursive type like Nat. Because of your modulo case (Next D A), you'd need a wrapper like:

data NextN : Nat -> Nat -> Nat -> Type where
  NextS : {auto prf :      (S a) `LTE` m}  -> NextN m a (S a) -- succ in mod m
  NextZ : {auto prf : Not ((S a) `LTE` m)} -> NextN m a Z     -- wrap around

LToN : Letter -> Nat
LToN A = 0
LToN B = 1
LToN C = 2
LToN D = 3

LNeighbour : Letter -> Letter -> Type
LNeighbour x y =
  let a = LToN x
      b = LToN y
  in Either (NextN 3 a b) (NextN 3 b a)

LNPair : Type
LNPair = (p : (Letter, Letter) ** LNeighbour (fst p) (snd p))

mklnpair : (a : Letter) -> (b : Letter) -> {auto prf : LNeighbour a b} -> LNPair
mklnpair a b {prf} = ((a, b) ** prf)

With NextS and NextZ you only have two cases you need to inspect instead one for each letter.

Upvotes: 4

Related Questions