好的。每当您遇到类型问题时,最好的方法是向编译器提供显式类型注释。由于day、month 和year 可能不会太大,所以将它们设为Ints 是个好主意。你显然也错过了一个大括号,我为你修好了:
day_of_year :: Int -> Int -> Int -> Int
day_of_year year month day = n1 - (n2 * n3) + day - 30
where
n1 = floor(275 * fromIntegral month / 9)
n2 = floor((month + 9) / 12)
n3 = 1 + floor((year - 4 * floor(fromIntegral year / 4) + 2) / 3)
当我尝试编译它时,GHC 会吐出这个相当长的错误消息:
bar.hs:8:16:
没有 (RealFrac Int) 的实例
由使用“地板”引起
可能的修复:为 (RealFrac Int) 添加实例声明
在 `(+)' 的第二个参数中,即
`floor ((year - 4 * floor (fromIntegral year / 4) + 2) / 3)'
在表达式中:
1 + floor ((year - 4 * floor (fromIntegral year / 4) + 2) / 3)
在“n3”的等式中:
n3 = 1 + floor ((year - 4 * floor (fromIntegral year / 4) + 2) / 3)
bar.hs:8:68:
没有 (Fractional Int) 的实例
由使用 `/' 引起
可能的解决方法:为 (Fractional Int) 添加实例声明
在“floor”的第一个参数中,即
`((年 - 4 * floor (fromIntegral year / 4) + 2) / 3)'
在 `(+)' 的第二个参数中,即
`floor ((year - 4 * floor (fromIntegral year / 4) + 2) / 3)'
在表达式中:
1 + floor ((year - 4 * floor (fromIntegral year / 4) + 2) / 3)
第二个错误是重要错误,第一个更多是后续。它本质上是说:Int 没有实现除法而不是floor。在 Haskell 中,整数除法使用不同的函数(div 或 quot),但您需要在此处进行浮点除法。由于year 被固定为Int,因此减数4 * floor(fromIntegral year / 4) + 2 也被固定为Int。然后除以 3,但如前所述,不能使用浮动除法。让我们通过在除法之前用fromIntegral 将整个术语“转换”为另一种类型来解决这个问题(就像您之前所做的那样)。
fromIntegral 具有签名(Integral a, Num b) => a -> b。这意味着:fromIntegral 接受一个整数类型的变量(例如Int 或Integer)并返回一个任意数值类型的变量。
让我们尝试编译更新后的代码。 n2的定义也出现了类似的错误,我也修复了:
day_of_year :: Int -> Int -> Int -> Int
day_of_year year month day = n1 - (n2 * n3) + day - 30
where
n1 = floor(275 * fromIntegral month / 9)
n2 = floor((fromIntegral month + 9) / 12)
n3 = 1 + floor(fromIntegral (year - 4 * floor(fromIntegral year / 4) + 2) / 3)
这段代码编译并运行良好(在我的机器上)。 Haskell 有某些类型默认规则,导致编译器选择 Double 作为所有浮点除法的类型。
实际上,您可以做得更好。用整数除法代替重复的浮点数转换怎么样?
day_of_year :: Int -> Int -> Int -> Int
day_of_year year month day = n1 - (n2 * n3) + day - 30
where
n1 = 275 * month `quot` 9
n2 = (month + 9) `quot` 12
n3 = 1 + (year - 4 * (year `quot` 4) + 2) `quot` 3
此算法应始终产生与上述浮点版本相同的结果。它可能快了大约十倍。反引号允许我使用函数 (quot) 作为运算符。
关于您的第六点:是的,这样做很容易。只需在year、month 和day 前面加上fromEnum。函数fromEnum :: Enum a => a -> Int 将任何枚举类型转换为Int。 Haskell 中所有可用的数字类型(复杂的 iirc 除外)都是 Enum 类的成员。但这不是一个好主意,因为您通常有Int 参数,并且多余的函数调用会减慢您的程序速度。更好地显式转换,除非您的函数预计与许多不同的类型一起使用。实际上,不要太担心微优化。 ghc 有一个复杂且有点神秘的优化基础架构,可以让大多数程序运行得非常快。
修改
跟进一、二、三
是的,你的推理是正确的。
跟进4
如果您不给day_of_year 的浮点变体提供类型签名,则其类型默认为day_of_year :: (Integral a, Integral a2, Integral a1) => a -> a1 -> a2 -> a2。这实质上意味着:day、month 和year 可以是实现Integral 类型类的任意类型。该函数返回一个与day 相同类型的值。在这种情况下,a、a1 和 a2 只是不同的类型变量 - 是的,Haskell 也有类型级别的变量(以及种类级别 [这是type],但那是另一回事)- 可以满足任何类型。所以如果你有
day_of_year (2012 :: Int16) (5 :: Int8) (1 :: Integer)
变量a 被实例化为Int16,a1 变为Int8 和a2 变为Integer。那么这种情况下的返回类型是什么?
是Integer,看看类型签名!
跟进 5
事实上,你既是又不是。使类型尽可能通用当然有其优点,但它会使类型检查器感到困惑,因为当没有显式类型注释的术语中涉及的类型过于通用时,编译器可能会发现不止一种可能键入一个术语。这可能会导致编译器通过一些标准化但有些不直观的规则来选择类型,或者它只是用一个奇怪的错误来迎接你。
如果您真的需要通用类型,请争取类似
day_of_year :: Integral a => a -> a -> a -> a
即:参数可以是任意的Integral 类型,但所有参数必须具有相同的类型。
永远记住 Haskell 从不强制类型转换。当涉及(自动)强制转换时,几乎不可能完全推断类型。您只能手动投射。现在有些人可能会告诉你Unsafe.Coerce 模块中的函数unsafeCoerce,它的类型为a -> b,但你实际上并不想知道。它可能不会像你认为的那样做。
跟进 6
div 没有任何问题。当涉及负数时,差异开始出现。现代处理器(如 Intel、AMD 和 ARM 制造的处理器)在硬件中实现 quot 和 rem。 div 也使用这些操作,但会进行一些操作以获得不同的行为。当您并不真正依赖负数的确切行为时,这会不必要地减慢计算速度。 (实际上有几台机器实现了div,但没有在硬件上实现quot。我现在唯一记得的是mmix)