如果类型安全是唯一的动机,那么将 Int (不是一般类型)包装在另一种类型中的正确方法是什么?

Enr*_*lis 0 haskell functional-programming type-safety newtype

我使用的是 a Map String (Int, Int),其中两个Ints 用作分子和分母来形成Rational要传递给 的a fromList

Int然后我意识到,在我的代码中的某个点上,我以相反的方式使用了这两个(作为分母和分子,即交换)。花了一些时间才找出问题所在,所以后来我想也许我应该使用两种专用类型,所以我写了

newtype MyNum = MyNum Int
newtype MyDen = MyDen Int
Run Code Online (Sandbox Code Playgroud)

但随后我必须添加一些实例才能使其他所有内容正常工作(考虑到我对这些Ints 的使用,我必须添加deriving (Eq, Ord, Show, Read)),并且还要添加一些两个函数来Int从两种类型中解开 s ,以便我可以实际上将类似的东西应用(+1)到那些包裹的Ints 上。

但这意味着代码开始看起来有点难看,比如(MyNum . (+1) . unwrapMyNum), 而类似的东西(+1) <$>会更好。

但这意味着MyNum应该是Functor;但它不能,因为它是硬类型,而不是类型构造函数。

但我不想将其设为类型构造函数,因为我不想在其中包含除Int.

有什么建议吗?

lef*_*out 8

我认为实际问题与你的具体问题无关。只是不要使用 tuples,使用合适的类型来表达两个整数一起表示的内容。在这种情况下,显而易见的选择是使用Ratio Int,但需要注意的是它不存储任意对,而是正确标准化分数(这通常是一件好事)。如果这不适合您,请编写您自己的Ratio类型。

也就是说,您还可以做很多事情来使单一类型的新类型包装器更加方便:

  • 派生实例。看来您仍在包装类型上使用数值运算。这很容易启用:

    {-# LANGUAGE DerivingStrategies #-}
    
    newtype MyNum = MyNum Int
     deriving stock (Eq, Ord, Show, Read)
     deriving newtype (Num, Enum, Real, Integral)
    
    Run Code Online (Sandbox Code Playgroud)

    现在MyNum基本上是功能齐全的克隆Int,可以直接编写negate (n + 9)for之类的表达式n :: MyNum,无需包装和展开。MyNum(但是当你将 a 传递给需要 a 的东西时,编译器仍然会犹豫MyDen。)

  • 单函子。如果您想强调MyNum作为的容器Int,而不是其本身的不同类型的数字类型,那么就可以使用该类MonoFunctor。你可以实例化

    {-# LANGUAGE TypeFamilies #-}
    
    newtype MyNum = MyNum Int
    
    type instance Element MyNum = Int
    
    instance MonoFunctor MyNum where
      omap f (MyNum i) = MyNum (f i)
    
    Run Code Online (Sandbox Code Playgroud)

    然后写eg omap (+1) n,它类似于(+1)<$>n但不要求容器支持除.之外的任何东西Int。还可以查看该包的其他类。

  • 通用包装/展开助手。有一些替代方法可以显式操作新类型或其他包含的数据,这些方法在特殊类不适合时也可以工作,并且比传统的访问器和构造函数对更方便。其中包括coerce镜头(更具体地说是Isos)。