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

Limit a number to a range (Haskell)

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

我想避免创建智能构造函数并返回 Maybe,因为这会使整个使用过程更加繁琐。

谢谢

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

虽然不知道会有多友好。请注意依赖类型在 GHC 8.0 中得到了改进,但我在这方面没有经验。如果您不希望它变得麻烦,我会确保您能够自如地使用模板 Haskell。

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

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

你可以使用

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

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

此解决方案使用依赖类型(并且可能过于重量级,请检查 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"

在这种情况下,我们使用像 someNatVal and isLE 这样的函数。这些函数可能会因 Nothing 而失败。但是,如果他们成功了,他们 return 一个 "witnesses" 某些约束的值。通过对见证进行模式匹配,我们将该约束纳入范围(这是有效的,因为见证是一个 GADT)。

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

因此,在某种程度上,我们已经将确保参数满足所需先决条件的所有繁琐工作转移到了函数本身之外。

您无需调用 Maybe 类型即可利用 'smart constructors'。如果愿意,您可以接受 (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)

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