Haskell type Promotion

I am currently working my way through. Write yourself a chart in 48 hours and am stuck in type promotion.

In short, the circuit has a number tower (Integer-> Rational-> Real-> Complex) with the numerical stocks that you would expect. I modeled the numbers with the obvious

data Number = Integer Integer | Rational Rational | Real Double | Complex (Complex Double) 

so using Rank2Types seemed like a simple way to get functions to work on this range of types. For Num a it looks like

 liftNum :: (forall a . Num a => a -> a -> a) -> LispVal -> LispVal -> ThrowsError LispVal liftNum fab = case typeEnum a `max` typeEnum b of ComplexType -> return . Number . Complex $ toComplex a `f` toComplex b RealType -> return . Number . Real $ toReal a `f` toReal b RationalType -> return . Number . Rational $ toFrac a `f` toFrac b IntType -> return . Number . Integer $ toInt a `f` toInt b _ -> typeErr ab "Number" 

which works, but quickly becomes verbose, because each class class requires a separate block.
Worse, this implementation of the complex is simplified, since the scheme can use separate types for the real and complex parts. To implement this, you will need a special version containing two Number , which will further aggravate verbosity if I want to avoid the recursive type.

As far as I know, there is no way to abstract from the context, so I hope for a cleaner way to implement this logic of numbers.

Thanks for reading!

+8
haskell
source share
1 answer

Here is a suggestion. The main thing is that we want your typeEnum function typeEnum execute something that does not yet bring the Num a dictionary into scope. So let me use GADT to make this happen. I will simplify a few things to simplify the explanation of the idea and write the code, but nothing significant: I will focus on Number , not LispVal , and I will not report detailed errors when everything goes wrong. First, some template:

 {-# LANGUAGE GADTs #-} {-# LANGUAGE Rank2Types #-} import Control.Applicative import Data.Complex 

Now you have not specified your type enumeration definition. But I will give mine, because it is part of a secret sauce: my enumeration type will have a connection between the Haskell level level and the Haskell type level via GADT.

 data TypeEnum a where Integer :: TypeEnum Integer Rational :: TypeEnum Rational Real :: TypeEnum Double Complex :: TypeEnum (Complex Double) 

Because of this join, my Number type will no longer repeat each of these cases. (I suspect your types typeEnum and Number pretty repeating compared to each other.)

 data Number where Number :: TypeEnum a -> a -> Number 

Now we are going to define a new type that you did not have, which will bind typeEnum together with the Num dictionary for the corresponding type. This will be the return type of our typeEnum function.

 data TypeDict where TypeDict :: Num a => TypeEnum a -> TypeDict ordering :: TypeEnum a -> Int ordering Integer = 0 -- lowest ordering Rational = 1 ordering Real = 2 ordering Complex = 3 -- highest instance Eq TypeDict where TypeDict l == TypeDict r = ordering l == ordering r instance Ord TypeDict where compare (TypeDict l) (TypeDict r) = compare (ordering l) (ordering r) 

The ordering function reflects (my guess) the direction in which drops can be performed. If you try to implement Eq and Ord yourself for this type without looking at my solution, I suspect you will find out why the GHC refuses to get these classes for GADT. (At least it took me a few tries! Obvious definitions do not check the type, and slightly less obvious definitions have the wrong behavior.)

Now we are ready to write a function that produces a dictionary for a number.

 typeEnum :: Number -> TypeDict typeEnum (Number Integer _) = TypeDict Integer typeEnum (Number Rational _) = TypeDict Rational typeEnum (Number Real _) = TypeDict Real typeEnum (Number Complex _) = TypeDict Complex 

We will also need a casting function; you can simply combine your definitions of toComplex and friends here:

 -- combines toComplex, toFrac, toReal, toInt to :: TypeEnum a -> Number -> Maybe a to Rational (Number Integer n) = Just (fromInteger n) to Rational (Number Rational n) = Just n to Rational _ = Nothing -- etc. to _ _ = Nothing 

Once we have this mechanism, liftNum is surprisingly short. We just find a suitable type for the throw, get a dictionary for that type, and perform the casts and the operation.

 liftNum :: (forall a. Num a => a -> a -> a) -> Number -> Number -> Maybe Number liftNum fab = case typeEnum a `max` typeEnum b of TypeDict ty -> Number ty <$> liftA2 f (to ty a) (to ty b) 

At this point, you can complain: your final goal was not to have one case for each instance of the class in liftNum , and we have achieved this goal, but it looks like we just dropped it into the definition of typeEnum , where there is one case for the instance class. However, I defend myself: you did not show us your typeEnum , which, I suspect, already had one case for each instance of the class. Thus, we did not incur any new costs in functions other than liftNum , and we really significantly simplified liftNum . It also gives a smooth upgrade path for more complex Complex manipulations: extend the definition of typeEnum , the cast ordering and to function, and you will go well; liftNum may remain unchanged. (If it turns out that the types are not linearly ordered, but instead of some kind of lattice or similar, then you can switch from the Ord class.)

+12
source share

All Articles