【问题标题】:multiplication of different types in HaskellHaskell中不同类型的乘法
【发布时间】:2026-02-07 01:15:01
【问题描述】:

我是haskell 新手,时不时遇到问题,我会尝试在这里用几句话来描述。想象一下,我想为不同的度量声明不同的类型,这样 Haskell 类型系统就会在我的公式中发现错误:

newtype Dist = Km Float
newtype Time = H Float
newtype Velocity = KmH Float

(/) :: Dist → Time → Velocity
(Km d) / (H t) = KmH (d / v)

(*) :: Velocity → Time → Dist
(KmH v) * (H t) = Km (v * t)

所以,每当我试图在我的公式中使用错误的度量单位时,编译器都会咬牙切齿。

问题是我不能像这样实现临时多态。使用这段代码,我带来了歧义——编译器可以区分我的运算符 * 和 Prelude 中定义的那个。也无法声明 Num 类的实例,因为我需要不同类型的参数。

我想知道人们通常如何解决问题。

提前致谢!

【问题讨论】:

  • 我曾经看到一个 Ada 项目试图这样做,但由于类型系统不符合它的要求,结果出现了可怕的错误。我建议您要么使用 Dimensional(它利用 Haskell 类型系统的强大功能来做到这一点),要么坚持类型同义词并依靠代码审查来发现缺陷。

标签: haskell types


【解决方案1】:

您可以根据需要隐藏通常的 (*),通过

import Prelude hiding((*))

或者隐藏所有的Nu​​m

import Prelude hiding(Num(..))

然后你可以定义你自己的乘法,大概是这样

class Mul a b c | a b -> c, b c -> a, c a -> b where
    (*) :: a -> b -> c

【讨论】:

  • 是的,我也想过。我想在这种情况下,使用我的模块的每个人都必须导入前奏隐藏一些东西?可以自动化吗?可能不会,因为例如Data.Foldable 模块在其文档中明确表示要在主机模块中解决此问题。
  • 不幸的是,它不能自动化。为了最大限度地减少模块用户的麻烦,您需要给乘法一个不同的名称。
  • 我明白你为什么有这个a b -> c。也就是说,cab 确定。如果是这样,考虑到(*) :: a -> b -> c,为什么还要有b c -> ac a -> b?请详细说明。
  • 为什么需要对Prelude 隐藏Num 的东西?来自它的(*) 会干扰Mul?我想正确的 impl 将由类型解决。
【解决方案2】:

您可以尝试重新制定您的单位系统。试试这样的:

data Unit = Unit String
          | Unit :/: Unit
          | Unit :*: Unit
          | Unit :^: Int
          deriving (Show,Eq)

instance Num Unit where
  -- Insert reasonable definition here
  x * y = ...

data UnitNum n = UN n Unit

instance Num (Num n) => UnitNum n where
  UN n u + Un k u' | u == u' = UN (n+k) u
                   | otherwise = error ...
  -- insert other definitions here.

km,h,kmh :: Unit

km = Unit "km"
h = Unit "h"
kmh = km / h

编辑:

dimensional 包中实现了与此类似但完全不同的东西。阅读源代码,它是读写 Haskell 并且很好理解。这段代码对于大多数科学应用来说应该已经足够好了。

【讨论】:

  • 请注意,此实现是纯动态的。有趣的部分是让编译器完成工作(就像 Dimension 一样)!
  • 抱歉有问题。 Unit :/: Unit 中的 :/: 是什么?
  • @Maxym:它是Unit的类型构造函数。
  • 我从未见过在两个冒号之间有一个中缀函数的 Data 构造函数。在 Haskell 中我还有很多东西要学。你能指点我这方面的任何文档吗?
  • 这是一个 GHC 扩展,每个以冒号开头的指示操作都被处理为数据构造函数。您也可以在类型级别使用这种名称,例如。 :-> 在 Haskell 中是一个有效的类型名称。
【解决方案3】:

通常的方法是创建一个不同的运算符来乘以你的类型——普通的* 已经被采用了。您可以使用字符!#$%&*+./<=>?@\^|-~ 的任意组合来定义自己的运算符。所以你可以使用|*|(TIE Fighter 操作员)和|/| 或类似的东西。

【讨论】:

    【解决方案4】:

    我这样做的方式(以避免维度包的类型级复杂性)基本上是您的新类型解决方案,但具有相当数量的辅助函数。

    class Units a where
        wrap :: Double -> a
        unWrap :: a -> Double
    
    instance Units Double where
        wrap = id
        unWrap = id
    
    inWrap :: (Units a) => (Double -> Double) -> a -> a
    inWrap f = wrap . f . unWrap
    
    newtype Years = Years Double deriving (Typeable, Show, Eq, Ord)
    instance Units Years where
        wrap = Years
        unWrap (Years x) = x
    
    newtype a :/: b = Per a deriving (Typeable, Show, Eq, Ord)
    instance Units a => Units (a :/: b) where
        wrap = Per . wrap
        unWrap (Per x) = unWrap x
    
    perYears :: a -> a :/: Years
    perYears = Per
    
    class Additive a where
        (+%) :: a -> a -> a
        negU :: a -> a
        (-%) :: a -> a -> a
        x -% y = x +% negU y
    
    instance Units a => Additive a
        where x +% y = wrap $ unWrap x + unWrap y
              negU = inWrap negate
    
    class ScalarMult f where
        (.*) :: f -> Double -> f
    class ScalarAdd f where
        (.+) :: f -> Double -> f
    
    instance Units a => ScalarAdd a where
        f .+ v = inWrap (+ v) f
    instance Units a => ScalarMult a where
        f .* v = inWrap (* v) f
    

    等等……

    【讨论】: