8

My aim is to eliminate () from terms, like this:

(a, b)       -> (a, b)
((), b)      -> b
(a, ((), b)) -> (a, b)
...

And this is the code:

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

module Simplify where

import Data.Type.Bool
import Data.Type.Equality

type family Simplify x where
  Simplify (op () x) = Simplify x
  Simplify (op x ()) = Simplify x
  Simplify (op x y)  = If (x == Simplify x && y == Simplify y)
                          (op x y)
                          (Simplify (op (Simplify x) (Simplify y)))
  Simplify x         = x

However, trying it out:

:kind! Simplify (String, Int)

...leads to an infinite loop in the type checker. I'm thinking the If type family should be taking care of irreducible terms, but I'm obviously missing something. But what?

Philip Kamenarsky
  • 2,757
  • 2
  • 24
  • 30
  • 4
    It seems you are assuming that type-level computation is lazy, hence that the second branch of the `If` won't be evaluated unless it is needed. I think that assumption is unwarranted. – Daniel Wagner Sep 08 '16 at 16:52
  • 6
    And by the way: being polymorphic over `op` is probably wrong. For example, `Simplify (Either () Int)` probably should not reduce to `Int`. – Daniel Wagner Sep 08 '16 at 16:54
  • What should the behaviour be on `(String, (), Int)`? Both solutions suggested so far reduce that to `Int`. I don't even know if it's possible to get `(String, Int)`. – gallais Sep 09 '16 at 19:18
  • @gallais: That's fine, terms can be (nested) tuples only. – Philip Kamenarsky Sep 12 '16 at 13:24

2 Answers2

10

Type family evaluation isn't lazy, so If c t f will evaluate all of c, t, and f. (In fact, type family evaluation order isn't really defined at all right now.) So it's no wonder you end up with an infinite loop – you always evaluate Simplify (op (Simplify x) (Simplify y)), even when that's Simplify (op x y)!

You can avoid this by splitting the recursion and the simplification apart, like so:

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

module Simplify where

import Data.Type.Bool
import Data.Type.Equality

type family Simplify1 x where
  Simplify1 (op () x) = x
  Simplify1 (op x ()) = x
  Simplify1 (op x y)  = op (Simplify1 x) (Simplify1 y)
  Simplify1 x         = x

type family SimplifyFix x x' where
  SimplifyFix x x  = x
  SimplifyFix x x' = SimplifyFix x' (Simplify1 x')

type Simplify x = SimplifyFix x (Simplify1 x)

The idea is:

  1. Simplify1 does one step of simplification.
  2. SimplifyFix takes x and its one-step simplification x', checks if they're equal, and if they aren't does another step of simplification (thus finding the fixed point of Simplify1).
  3. Simplify just starts off the SimplifyFix chain with a call to Simplify1.

Since type family pattern matching is lazy, SimplifyFix correctly delays evaluation, preventing infinite loops.

And indeed:

*Simplify> :kind! Simplify (String, Int)
Simplify (String, Int) :: *
= (String, Int)

*Simplify> :kind! Simplify (String, ((), (Int, ())))
Simplify (String, ((), (Int, ()))) :: *
= ([Char], Int)
Antal Spector-Zabusky
  • 36,191
  • 7
  • 77
  • 140
  • 3
    Wow, I was writing up an answer, too... and came up with *exactly* the same choice for second test case. For what it's worth, I think my answer is slightly simpler, though perhaps less generalizable: `Simplify1 ((), y) = y; Simplify1 (x, ()) = x; Simplify1 other = other; Simplify (x, y) = Simplify1 (Simplify x, Simplify y); Simplify other = other`. – Daniel Wagner Sep 08 '16 at 17:00
  • @DanielWagner I mean, you have to nest the `()`s at least two deep for a real test, right? :-) And it's nice to compare your solution! – Antal Spector-Zabusky Sep 08 '16 at 17:08
  • 3
    Also, a fun bit of magic: `:kind! forall a. Simplify1 (a, ()) = a`. That takes some real cleverness, given that it has to notice the first two clauses of `Simplify1` will give the same answer! – Daniel Wagner Sep 08 '16 at 17:11
  • @DanielWagner: … this baffles me. I really don't feel like that should work at all! – Antal Spector-Zabusky Sep 08 '16 at 17:13
  • 1
    @DanielWagner, it probably reuses some of the same-RHS logic used to check open type families. A bit unexpected, and I wonder just where it shouts "Surprise!" and breaks down, but definitely cool. – dfeuer Sep 08 '16 at 17:45
  • @DanielWagner Aren't the clauses always tried in-order, in _closed_ type families? – chi Sep 08 '16 at 18:24
  • @chi: Yes, but usually type variables don't reduce at all, because we don't know if they'll unify with the patterns or not. E.g., given `type family F x where { F () = Bool ; F a = a }`, then `:kind! forall a. F a` gives `forall a. F a :: * = F a`, because in some sense it's waiting to see if `a` is equal to `()` or not. But apparently, if the RHS is the same in all cases, then it reduces somehow! E.g., given `type family G x where { G () = () ; G a = a }`, you indeed have `:kind! forall a. G a` producing `forall a. G a :: * = a`. So this is (to me) quite astonishing. – Antal Spector-Zabusky Sep 08 '16 at 18:27
  • @AntalSpector-Zabusky Ah, I misread `Simplify1 ((), a) = a` and couldn't see the issue. Intriguing. – chi Sep 08 '16 at 18:30
  • @AntalSpector-Zabusky My guess is, whenever `F p = p' ; F q = q'`, compute the MGU `m` of `p,q` and check whether under that we have `p'[m]=q'[m]` syntactically. If so, add a third clause before the two stating `F p[m] = p'[m]`. The syntactic check could be smarter, of course -- this is a fragile heuristics, but I can't believe GHC can afford much more than this. – chi Sep 08 '16 at 18:34
  • @chi What is the MGU of `(x,())` and `((),y)`? Is it not `x ~ (), y ~ ()`? If so, how does an extra clause `Simplify1 ((), ()) = ()` at the start help? – Daniel Wagner Sep 08 '16 at 18:46
3

I thought I'd mention that given that the simplification has the structure of a fold, there is no need to build this complex solution involving a fixpoint which re-traverses the expression again and again.

This will do just fine:

{-# LANGUAGE TypeFamilies         #-}
{-# LANGUAGE UndecidableInstances #-}
module Simplify where

type family Simplify x where
  Simplify (op a b) = Op op (Simplify a) (Simplify b)
  Simplify x        = x

type family Op op a b where
  Op op () b = b
  Op op a () = a
  Op op a b  = op a b
gallais
  • 11,823
  • 2
  • 30
  • 63