A type that is easy to do with arithmetic and guaranteed to be within

Feel me ... this problem requires some explanation, but I think it is an interesting problem, and I believe that others have encountered it.

I would like to have a type which, as I know, will always have a value from 0 to 1 inclusive. This is easy enough to do, I can create the UnitInterval type and set only my intelligent constructor toUnitInterval and deconstructor fromUnitInterval .

 {-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | A number on the unit interval 0 to 1, inclusive. newtype UnitInterval = UnitInterval Double deriving (Show, Eq, Ord, Fractional, Floating) -- | Convert a value to a @ UnitInterval@. The value will be capped to -- the unit interval. toUnitInterval :: Double -> UnitInterval toUnitInterval = UnitInterval . max 0 . min 1 fromUnitInterval :: UnitInterval -> Double fromUnitInterval (UnitInterval x) = x 

So far so good. But users of my module will find that arithmetic with UnitInterval and Double is messy. For instance,

 λ> let a = toUnitInterval 0.5 λ> let b = 0.25 :: Double λ> toUnitInterval $ (fromUnitInterval a) * b UnitInterval 0.125 

Of course, I could make UnitInterval derived instance of Num , so I can easily do arithmetic while I stick to UnitInterval s.

 λ> a*a UnitInterval 0.25 λ> a+a+a UnitInterval 1.5 -- Oops! out of range 

But I could write my own Num implementation for UnitInterval s, where type + operations check boundaries. But users of my module will need to perform complex calculations when partial results are not in range. Thus, they will have to convert everything to Double s, perform the calculations, and return to UnitInterval at the end.

But wait ... maybe there is a better way. I could make UnitInterval functor! An expression of the type fmap (\x -> x * exp x) a should give the result UnitInterval 0.8243606353500641 . Nice, clean code. Now Functor has the form (* → *) , and UnitInterval has the form * . But I can change it, like this ...

 {-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | A number on the unit interval 0 to 1, inclusive. newtype UnitInterval a = UnitInterval a deriving (Show, Eq, Ord, Num, Fractional, Floating) -- | Convert a value to a @ UnitInterval@. The value will be capped to -- the unit interval. toUnitInterval :: (Num a, Ord a) => a -> UnitInterval a toUnitInterval = UnitInterval . max 0 . min 1 fromUnitInterval :: UnitInterval a -> a fromUnitInterval (UnitInterval x) = x instance Functor UnitInterval where fmap f (UnitInterval x) = toUnitInterval (fx) -- line 16 

But this does not compile. And looking back, I see this because I will need to limit the result of fmap , which would give it a signature of a different type than the one that was in Functor .

 amy.hs:16:29: No instance for (Num b) arising from a use of 'toUnitInterval' Possible fix: add (Num b) to the context of the type signature for fmap ∷ (a → b) → UnitInterval a → UnitInterval b In the expression: toUnitInterval (fx) In an equation for 'fmap': fmap f (UnitInterval x) = toUnitInterval (fx) In the instance declaration for 'Functor UnitInterval' 

Sigh ... back to the first version, with ugly arithmetic. Does anyone have a better solution?

+6
source share
2 answers

You will encounter some difficulties with what you ask because the numbers on [0, 1] are not closed in the operation (+) . In other words, “within [0, 1]” the warranty is not retained by adding.

So, there are several ways to interpret what you want. One of them is that you can search for “phases” of the operation between each one you override the values ​​on [0, 1]

 mapConstrain :: (Num a, Ord a) => (a -> a) -> (UnitInterval a -> UnitInterval a) mapConstrain f (UnitInterval val) = UnitInterval (max 0 (min 1 (f val))) 

Performing such operations will limit you, as you can write something like

 a :: UnitInterval Double b :: UnitInterval Double a + b 

using mapConstrain . Applicative typeclass offers a mechanism to alleviate this problem, however.

Another way forward is to make a limit after each operation. Then we could create an instance of Num

 newtype UnitInterval a = UI a constrain :: (Num a, Ord a) => a -> a constrain = max 0 . min 1 instance Num a => Num (UnitInterval a) where UI a + UI b = UI (constrain $ a + b) UI a * UI b = UI (constrain $ a * b) -- not technically needed! abs (UI a) = UI a signum (UI a) = UI (signum a) ... 

The last way forward is to allow unlimited operations, but only allows users to “view” UnitInterval values ​​that are valid. This is probably the easiest implementation because you automatically get Num

 newtype UnitInterval a = UI a deriving Num getUI :: (Num a, Ord a) => UnitInterval a -> Maybe a getUI (UI a) = if (a <= 1 && a >= 0) then Just a else Nothing 

Alternatively, you can just click on it with the last constrain . Of course, this mode of operation allows UnitInterval values ​​to go beyond [0, 1] until they return there before viewing.

+5
source

When I think of numbers from zero to one, I tend to think of one of two things:

Rational numbers

 data SM = SM {diff :: Natural, den :: Natural} toRatio :: SM -> Ratio Natural toRatio (SM diff den) = (1 + den + diff) / (1 + den) instance Eq SM where SM diffx denx == SM diffy deny = diffx * (deny + 1) == diffy * (denx + 1) 

Such numbers are really guaranteed to lie in the correct range.

Computable real numbers

It is much more terrible, but there is a package for them. Roughly speaking, you can represent numbers from 0 to 1 as infinite streams of bits. Implementing multiplication will be an interesting task; if you somehow manage to figure out the division, you don’t have to worry about division by zero, because it will just work forever.

+1
source

Source: https://habr.com/ru/post/988171/


All Articles