将数字限制在一个范围内(Haskell)

时间:2016-10-01 18:44:28

标签: haskell types functional-programming type-theory hindley-milner

我正在公开一个带有两个参数的函数,一个是最小边界,另一个是最大边界。如何使用类型确保例如最小边界不大于最大边界?

我想避免创建一个智能构造函数并返回一个Maybe,因为它会使整个用法更麻烦。

谢谢

4 个答案:

答案 0 :(得分:13)

这并不完全回答您的问题,但有时可行的一种方法是更改​​您的类型的解释。例如,而不是

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

你可以使用

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

这样,就无法表示无效范围。

答案 1 :(得分:9)

此解决方案使用依赖类型(可能过于重量级,请检查 dfeuer 的答案是否足以满足您的需求)。

该解决方案使用 base 中的GHC.TypeLits模块以及typelits-witnesses包的模块。

这是一个差异函数,它接受两个整数参数(静态已知)并在编译时抱怨第一个数字大于第二个:

{-# 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

我们可以从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 **

但是如果我们想在运行时使用具有确定值的函数呢?比方说,我们从控制台或文件中读取的值?

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"

在这种情况下,我们使用someNatValisLE等功能。这些功能可能会因Nothing而失败。然而,如果他们成功了,他们会返回一个值得见证的值。一些约束。通过证人的模式匹配,我们将该约束纳入范围(这是因为证人是GADT)。

在该示例中,Just (SomeNat proxyn,SomeNat proxym)模式匹配将两个参数的KnownNat约束带入范围。 Just Refl模式匹配将n <= m约束带入范围。只有这样我们才能调用difference函数。

因此,在某种程度上,我们已经转移了所有繁忙的工作,确保参数满足函数本身所需的前提条件。

答案 2 :(得分:1)

您要求的是依赖类型。有一个很好的教程 https://www.schoolofhaskell.com/user/konn/prove-your-haskell-for-great-safety/dependent-types-in-haskell

虽然我不知道它会有多友好。请注意,GHC 8.0中的依赖类型有所改进,但我没有该领域的经验。如果你不想让它变得麻烦,我会确保你习惯使用模板Haskell。

答案 3 :(得分:1)

您无需调用Maybe类型来利用'智能构造函数'。如果您愿意,可以接受(min,max)(max,min)形式的构造函数,并仍然创建一个data类型,正确解释哪个是哪个。

例如,你可以制作一个小模块:

module RangeMinMax (
               Range,
               makeRange
              )
where

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

现在,当您使用Range创建makeRange时,将自动排列2元组,使其形式为(min,max)。请注意,Range的构造函数未导出,因此模块的用户无法创建无效的Range - 您只需确保在此模块中创建有效的。{ / p>