Limit a number to a range (Haskell)

耗尽温柔 提交于 2019-11-30 19:42:22

This doesn't exactly answer your question, but one approach that sometimes works is to change your interpretation of your type. For example, instead of

data Range = {lo :: Integer, hi :: Integer}

you could use

data Range = {lo :: Integer, size :: Natural}

This way, there's no way to represent an invalid range.

This solution uses dependent types (and might be too heavyweight, check if dfeuer's answer is enough for your needs).

The solution makes use of the GHC.TypeLits module from base, and also of the typelits-witnesses package.

Here is a difference function that takes two integer arguments (known statically) and complains at compile-time when the first number is greater than the second:

{-# language TypeFamilies #-}
{-# language TypeOperators #-}
{-# language DataKinds #-}
{-# language ScopedTypeVariables #-}

import GHC.TypeLits
import GHC.TypeLits.Compare
import Data.Type.Equality
import Data.Proxy
import Control.Applicative

difference :: forall n m. (KnownNat n,KnownNat m,n <= m) 
           => Proxy n 
           -> Proxy m 
           -> Integer
difference pn pm = natVal pm - natVal pn

We can check it from GHCi:

ghci> import Data.Proxy
ghci> :set -XTypeApplications
ghci> :set -XDataKinds
ghci> difference (Proxy @2) (Proxy @7)
5
ghci> difference (Proxy @7) (Proxy @2)
** TYPE ERROR **

But what if we want to use the function with values determined at run time? Say, values that we read from console, or from a file?

main :: IO ()
main = do
   case (,) <$> someNatVal 2 <*> someNatVal 7 of
       Just (SomeNat proxyn,SomeNat proxym) ->
            case isLE proxyn proxym of
                Just Refl -> print $ difference proxyn proxym 
                Nothing   -> error "first number not less or equal"
       Nothing ->     
            error "could not bring KnownNat into scope"

In this case, we use functions like someNatVal and isLE. These functions might fail with Nothing. If they succeed, however, they return a value that "witnesses" some constraint. And by pattern-matching on the witness, we bring that constraint into scope (this works because the witness is a GADT).

In the example, the Just (SomeNat proxyn,SomeNat proxym) pattern match brings KnownNat constraints for the two arguments into scope. And the Just Refl pattern match brings the n <= m constraint into scope. Only then we can call our difference function.

So, in a way, we have shifted all the busywork of ensuring that the arguments satisfy the required preconditions out of the function itself.

What you're asking for is dependent types. There is a nice tutorial on it in https://www.schoolofhaskell.com/user/konn/prove-your-haskell-for-great-safety/dependent-types-in-haskell

Although I don't know how friendly it will be. Do note that dependent typing was improved in GHC 8.0 but I have no experience in that area. I would make sure you're comfortable using template Haskell if you don't want it to be cumbersome.

You needn't invoke the Maybe type to take advantage of 'smart constructors'. If you like, you may accept constructors of either form (min,max) or (max,min) and still create a data type which correctly interprets which is which.

For instance, you could make a little module:

module RangeMinMax (
               Range,
               makeRange
              )
where

data Range = Range (Integer,Integer)
  deriving Show
makeRange a b = Range (min a b, max a b)

And now when you create a Range using makeRange, the 2-tuple will automatically be arranged so it's in the form (min,max). Note that the constructor for Range is not exported, so the user of the module is unable to create an invalid Range-- you just need to make sure that you create valid ones in the this module.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!