Reputation: 22596
I'm trying to multiply array of units (from dimensional
) in a phantom type and I'm having trouble with functional dependencies. A simplified version of the problem is the following :
I have the following type
data F (a:: [*]) = F String
Where the string represent an expression in a foreign language and a phantom type representing a list of types.
I can do things like
x = F "x" :: F '[Double]
y = F "(1,3)" :: F '[Int, Int]
I managed to implements arithmetics operators like this by creating a Nums
class which is a list of Num
.
class Nums (a::[*])
instance Nums '[]
instance (Num a, Nums as) => Num (a ': as)
Then I can do instanciate Num F
instance Nums as => F as where
(F a) * (F b) = F (a ++ "*" ++ b)
... etc ...
Now, I'm trying to do the same using physical units. I can do it with a list of one type this way
import qualified Numeric.Units.Dimensional as Dim
data F (a::[*]) = F String
(!*!) :: (Num n, Dim.Mul a b c) => F '[Dim.Dimensional v a n]
-> F '[Dim.Dimensional v b n]
-> F '[Dim.Dimensional v c n]
(F a) !*! (F b) = F (a ++ "*" ++ b)
That's seem to work, I can "multiply" 2 'F's of different units, and the result is in the corret unit.
Obviously, I want to generalize this to any list and use the same tricks as Nums
which I call Muls
.
class Muls a b c | a b -> c
instance '[] '[] '[]
instance (Num n, Mul a b c, Muls as bs cs)
=> Muls (Dim.Dimensional v a n ': as)
(Dim.Dimensional v b n ': bs)
(Dim.Dimensional v c n ': cs)
!*! :: (Muls as bs cs) => F as -> F bs -> F cs
(F a) !*! (F b) = F (a ++ "*" ++ b)
I got an Illegal Instance declaration
error :
Illegal instance declaration for
‘Muls
(Dim.Dimensional v a n : as)
(Dim.Dimensional v b n : bs)
(Dim.Dimensional v c n : cs)’
The coverage condition fails in class ‘Muls’
for functional dependency: ‘a b -> c’
Reason: lhs types ‘Dim.Dimensional v a n : as’, ‘Dim.Dimensional
v b n
: bs’
do not jointly determine rhs type ‘Dim.Dimensional v c n : cs’
Using UndecidableInstances might help
In the instance declaration for
‘Muls (Dim.Dimensional v a n : as) (Dim.Dimensional v b n
: bs) (Dim.Dimensional v c n : cs)’
If I use the UndecidableInstances
extension, it seems to work indeed. My question, is why do I need this extension and is there a way I could avoid it ?
Alternatively, I probably could get this working using the type family version of dimensional
. Unfortunatly, I need custom units and it's not clear if dimensional-tf
supports user defined units.
Upvotes: 4
Views: 185
Reputation: 14578
By default Haskell requires that instance selection be decidable, ie, that an attempt to decide if an type satisfy a constraint cannot cause an infinite loop in the compiler. Consider the following code:
class A (a :: *)
class B (a :: *)
instance A a => B a
This obviously instance could cause an infinite loop (it won't do so necessarily!). Even if every other instance by itself can't cause an infinite loop, the addition of this instance can. There is probably a rigorous proof of this somewhere but I don't know it.
The only thing UndecidableInstances
does is say "I promise I will never call my functions with types which cause infinite loops, so even though my instances can produce infinite loops, I accept responsibility for making sure that doesn't happen."
On the other hand, instances of the form:
instance (C1 a1, C2 a2 ... Cn an) => C (T a1 a2 .. an)
Will never produce an infinite loop, since Haskell doesn't allow infinite types, and this instance unpacks a single constructor, so even if Ci
refers back to C
you will eventually get to a type constructor which has 0 type parameters.
If you write an instance which is not decidable, you should get
test.hs:26:10:
Constraint is no smaller than the instance head
in the constraint: A a
(Use UndecidableInstances to permit this)
In the instance declaration for `B a'
I think this is the error you should see in your case and that displaying the error you actually see should be considered a bug.
Upvotes: 2