如果 i、n 和 k 是有理数,则可以走无限精度路线:
f :: (Rational, Rational) -> Rational -> [Rational]
f (i, n) k = [i, (i+k) .. n]
这种表示法可能需要一点时间来适应:
ghci> f (0%1, 1%1) (1%10)
[0 % 1,1 % 10,1 % 5,3 % 10,2 % 5,1 % 2,3 % 5,7 % 10,4 % 5,9 % 10,1 % 1]
将% 想象成一个有趣的分数栏。
您可以查看近似值
import Control.Monad (mapM_)
import Data.Ratio (Rational, (%), denominator, numerator)
import Text.Printf (printf)
printApprox :: [Rational] -> IO ()
printApprox rs = do
mapM_ putRationalToOnePlaceLn rs
where putRationalToOnePlaceLn :: Rational -> IO ()
putRationalToOnePlaceLn r = do
let toOnePlace :: String
toOnePlace = printf "%.1f" (numFrac / denomFrac)
numFrac, denomFrac :: Double
numFrac = fromIntegral $ numerator r
denomFrac = fromIntegral $ denominator r
putStrLn toOnePlace
上面的代码是用带有完整类型注释的命令式风格编写的。将其类型理解为将有理数列表转换为一些 I/O 操作。来自Control.Monad 的mapM_ 组合子为列表中的每个值(我们想要近似的有理数)评估一个动作(在本例中为putRationalToOnePlaceLn)。您可以将其视为一种for 循环,甚至还有一个forM_ 组合子与mapM_ 相同,只是参数的顺序颠倒了。最后的下划线是 Haskell 约定,表明它丢弃了运行操作的结果,并注意有 mapM 和 forM 确实收集了这些结果。
要通过putStrLn 安排近似值的输出,我们必须生成一个字符串。如果你是用 C 语言编写的,你会有类似
的代码
int numerator = 1, denominator = 10;
printf("%.1f\n", (double) numerator / (double) denominator);
上面的 Haskell 代码在结构上是相似的。 Haskell 的/ 运算符的类型是
(/) :: (Fractional a) => a -> a -> a
这表示对于 Fractional 类型类的某些实例 a,当给定两个相同类型的值 a 时,您将返回该类型的另一个值。
我们可以让ghci 告诉我们有关Fractional 的信息:
ghci> :info 小数
class (Num a) => 小数 a where
(/) :: a -> a -> a
收据 :: a -> a
来自Rational :: Rational -> a
-- 在 GHC.Real 中定义
instance Fractional Float -- 在 GHC.Float 中定义
instance Fractional Double -- 在 GHC.Float 中定义
注意底部的instance 行。这意味着我们可以
ghci> (22::Float) / (7::Float)
3.142857
或
ghci> (22::Double) / (7::Double)
3.142857142857143
但不是
ghci> (22::Double) / (7::Float)
:1:16:
无法将预期类型“Double”与推断类型“Float”匹配
在`(/)'的第二个参数中,即`(7 :: Float)'
在表达式中:(22 :: Double) / (7 :: Float)
在 `it' 的定义中:it = (22 :: Double) / (7 :: Float)
当然不会
ghci> (22::Integer) / (7::Integer)
:1:0:
(小数整数)没有实例
由在 :1:0-27 使用 `/' 引起
可能的解决方法:为 (Fractional Integer) 添加实例声明
在表达式中: (22 :: Integer) / (7 :: Integer)
在'it'的定义中:it = (22 :: Integer) / (7 :: Integer)
请记住,Haskell 的 Rational 类型被定义为 Integers 的比率,因此您可以将 fromIntegral 视为 C 中的类型转换。
即使在阅读A Gentle Introduction to Haskell: Numbers 之后,您仍然可能会发现 Haskell 对混合数字类型非常挑剔。对于在头脑中或在纸上执行无限精度算术的我们来说,很容易忘记计算机只有有限精度并且必须处理近似值。类型安全是一种有用的现实检查。
样本输出:
*Main> printApprox $ f (0%1, 1%1) (1%10)
0.0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9
1.0
printApprox 的定义似乎对所有有用的路标(例如函数和参数的名称或类型注释)感到满意。随着您对 Haskell 的经验和习惯越来越熟悉,这种看起来很命令式的定义将开始显得杂乱无章。
Haskell 是一种函数式语言:它的优势在于通过将简单的函数组装成更复杂的函数来指定什么,而不是如何。曾经有人建议 Haskell 对函数的操作与 Perl 对字符串的操作一样强大。
在point-free style 中,参数消失了,留下了计算的结构。学习阅读,这种风格确实需要练习,但你会发现它有助于编写更简洁的代码。
通过调整导入,我们可以定义一个无点等价物,例如
import Control.Arrow ((***), (&&&))
import Control.Monad (join, mapM_)
import Data.Ratio (Rational, (%), denominator, numerator)
import Text.Printf (printf)
printApproxPointFree :: [Rational] -> IO ()
printApproxPointFree =
mapM_ $
putStrLn .
toOnePlace .
uncurry (/) .
join (***) fromIntegral .
(numerator &&& denominator)
where toOnePlace = printf "%.1f" :: Double -> String
我们看到了一些熟悉的部分:我们的新朋友 mapM_、putStrLn、printf、numerator 和 denominator。
还有一些奇怪的东西。 Haskell 的$ 运算符是编写函数应用程序的另一种方式。它的定义是
f $ x = f x
在您尝试之前,它可能看起来并不是非常有用
Prelude> 显示 1.0 / 2.0
:1:0:
(小数字符串)没有实例
由在 :1:0-13 使用 `/' 引起
可能的解决方法:为 (Fractional String) 添加一个实例声明
在表达式中:显示 1.0 / 2.0
在 `it' 的定义中:it = show 1.0 / 2.0
你可以把那行写成
show (1.0 / 2.0)
或
show $ 1.0 / 2.0
因此您可以将$ 视为另一种编写括号的方式。
然后是.,表示函数组合。它的定义是
(f . g) x = f (g x)
我们也可以这样写
(f . g) x = f $ g x
如您所见,我们应用右手函数,然后将结果提供给左手函数。您可能还记得数学教科书中的定义,例如
选择名称. 是因为它在外观上与凸起的圆点相似。
因此,对于一系列函数组合,从头到尾阅读通常最容易理解。
(numerator &&& denominator) 位使用来自Control.Arrow 的fan-out combinator。例如:
ghci> (分子 &&& 分母) $ 1%3
(1,3)
因此,它将两个函数应用于相同的值,并返回一个包含结果的元组。请记住,我们需要将fromIntegral 应用于分子和分母,这就是join (***) fromIntegral 所做的。请注意,*** 也来自 Control.Arrow 模块。
最后,/ 运算符采用单独的参数,而不是元组。命令式思考,你可能想写类似
(fst tuple) / (snd tuple)
在哪里
fst (a,_) = a
snd (_,b) = b
但从功能上思考!如果我们能以某种方式将/ 转换为一个接受元组并将其组件用作除法参数的函数会怎样?这正是uncurry (/) 所做的!
您在 Haskell 方面迈出了伟大的第一步。旅途愉快!