重载 (*) 作为 a -> b -> c

Overload (*) as a -> b -> c

具有以下类型

newtype Kms =
  Kms Double
newtype Hours =
  Hours Double
newtype KmsPerHour =
  KmsPerHour Double

我想要以下

foo :: KmsPerHour -> Hours -> Kms
foo kmsPerHour hours = kmsPerHour * hours

这可能吗?在一个完美的世界中,该解决方案也将支持 (/) 以及几个不同的单位(例如,m,m/s,m/s/s)

这样我可以更轻松地确保我使用的所有单位都匹配并且计算是正确的。 我现在这样做的方式(但有更多类型的不同组合的单位)是

foo :: KmsPerHour -> Hours -> Kms
foo (KmsPerHour kmsPerHour) (Hours hours) = Kms $ kmsPerHour * hours

我检查了这个 Can you overload + in haskell? and https://hackage.haskell.org/package/alg-0.2.10.0/docs/src/Algebra.html#%2B 但那些只是 a->a->a

更新

我的尝试是这样的。我真的很喜欢它如何非常紧密地保护类型并且它支持嵌套类型并且我不需要定义每个类型组合但我不知道这是否是一个好方法 - 特别是因为类型系列选项看起来很优雅。

Ofc,下面的函数可以改成operators

class Unit a where
  unwrap :: a -> Double
  unitMap :: (Double -> Double) -> a -> a

instance Unit Kms where
  unwrap (Kms x) = x
  unitMap f (Kms x) = Kms $ f x

newtype Per a b =
  Per a
  deriving (Eq, Show)

instance (Unit a, Unit b) => Unit (Per a b) where
  unwrap (Per x) = unwrap x
  unitMap f (Per x) = Per $ unitMap f x

multWithUnits :: (Unit a, Unit b) => Per a b -> b -> a
multWithUnits (Per x) z =
  let zVal :: Double
      zVal = unwrap z
   in unitMap (* zVal) x

divWithUnits :: (Unit a, Unit b) => a -> b -> Per a b
divWithUnits x y =
  let yVal = unwrap y
   in Per (unitMap (/ yVal) x)

multUnitWith :: (Unit a, Unit b) => Double -> Per a b -> Per a b
multUnitWith factor = convert (* factor)

divUnitWith :: (Unit a, Unit b) => Double -> Per a b -> Per a b
divUnitWith factor = convert (/ factor)

toKmsPerHour :: Kms -> Hours -> Per Kms Hours
toKmsPerHour kms h = km `divWithUnits` h

distance :: Per Kms Hours -> Hours -> Kms
distance speed time = speed `multWithUnits` time

我省略了 Hours 的实现,以及 Num、Ord 和其他东西的实例,以免膨胀 post。

addKms :: Kms -> Kms -> Kms
addKms k1 k2 = k1 + k2

想法?

不,不可能为 Prelude 的乘法运算符指定类型。但是您可以创建自己的运算符并为其指定您想要的任何类型。如果需要,您甚至可以将其命名为 (*)...

import Prelude hiding ((*))
import qualified Prelude
KmsPerHour a * Hours b = Kms (a Prelude.* b)

a -> b -> c 本身显然是一个太弱的签名,因为它也允许像

这样的东西
  (1`KmPerHour`) * (1`KmPerHour`) :: Hours

您确实需要对类型进行一些限制。大致有两种选择:

  • 多参数类型 class 具有 fundeps。

    {-# LANGUAGE FunctionalDependencies #-}
    
    infixl 7 ·
    class PhysQttyMul a b c | a b -> c, a c -> b, b c -> a where
      (·) :: a -> b -> c
    
    newtype Length = Kms {getLengthInKm :: Double} -- deriving Data.VectorSpace.VectorSpace
    newtype Time = Hours {getTimeInHours :: Double}
    newtype Speed = KmPerHour {getSpeedInkmph ::Double}
    
    instance PhysQttyMul Speed Time Length where
      KmPerHour v · Hours t = Kms $ v*t
    instance PhysQttyMul Time Speed Length where
      Hours v · KmPerHour t = Kms $ v*t
    
  • 一个类型族,只在编译时计算两个操作数的乘积类型。

    type family PhysQttyProd a b :: *
    
    type instance PhysQttyProd Speed Time = Length
    type instance PhysQttyProd Time Speed = Length
    

    ...然后你仍然需要一个类型class来进行实际的值乘法

    class PhysQttyMul a b where
      (·) :: a -> b -> PhysQttyProd a b
    
    instance PhysQttyMul Speed Time where
      KmPerHour v · Hours t = Kms $ v*t
    instance PhysQttyMul Time Speed where
      Hours v · KmPerHour t = Kms $ v*t
    

后一个选项看起来复杂得多,但在实际定义单位系统时有一些实际优势。特别是,它非常适合可以通过一组基本单位的指数来表达 any 物理量的通用类型。 the units library就是这样做的。