乘法口诀表

编程应该是有趣的

这次我们要完成一个“极具实用性”的功能 —- 显示乘法口诀表!
(哦。真的是“太”实用的功能了。)

这表示我们的人工智能智力水平已经达到小学程度了!(误。)

除此之外,我们还请到了我另一个人格 —- 括号菌—- 作为我们的评论嘉宾,希望你会喜欢他的风格(没错就是我。)

首先,让我们仔细思考一下乘法口诀表的真正意义,(这玩意儿还能有啥意义?)
它其实是两列数字的乘积,就像这样:

  1. * 1 2 3 4 5 6 7 8 9
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5
  7. 6
  8. 7
  9. 8
  10. 9

我们只要让它们相乘,这个表就出来了~(嗯,那要怎么让程序去乘呢。)

首先,我们拿 1 去和 1 相乘,再拿 1 和 2 相乘 ……这样就产生了第一列,然后再拿 2去乘,得到第二列。(那么我们就要用一个循环咯?)

没错,循环是可以的,但是,我们有一个更为强大的东西,循环什么的,太落后了。(那是?)

下面有请(可能是)函数世界里最常用的高阶函数:

map (名气大,字都跟着变大了呢)

下面请看前方记者带回的报道

map 先生,请问为什么他们都说你很强大呢?

嗯,除了英俊的外表之外,鄙人可以使用函数来一次性的处理一大批数据,这个功能很强吧。
 
呃,这个,能用浅显的话给我们解释一下么?

当然。你告诉我一个函数,然后告诉我一个列表,我就会把那个列表里面的值,依次作为你告诉我的那个函数的参数来执行,最后,把每次执行的结果构成一个列表来返回。
 
好像没听懂啊……

没关系,我来演示一下

  1. => (map * [4 2 5] [1 6 3])
  2. (4 12 15)

你看,我现在有一个乘法函数,以及一批参数,我现在拿出这批参数的第一组,4 和 1,然后把它们传递给乘法函数作为参数,乘法函数返回的结果为 4,于是我就把 4 放在结果列表的第一个位置。接下来我继续取出这批参数的第二组,2 和 6,经过乘法函数的处理,结果是12,于是我就把 12 放在结果列表的第二个位置……
 
哦!我明白了 map 先生!不过,如果这批参数的数量并不相等,那怎么办呢?

唔,这个嘛,我只能尽量保证能被处理的内容被处理了,多出的部分我只能简单抛弃了。你看。

  1. => (map * [4 2] [1 6 3])
  2. (4 12)

也就是说,依赖于前面那个参数列表对么?

你总结的不错!不过,这个乘法啊,一次接受两个参数,所以后面才有两组参数列表。如果有个函数一次接受一个参数,那后面就只能跟着一个参数列表咯,也就是参数列表的数量要和函数一致,而且我也是会按次序把每个参数列表里的内容传递到函数中去的。比如 inc 函数,它就只能接受一个参数,返回这个参数加一之后的值:

  1. => (map inc [-10 11])
  2. (-9 12)

好的!感谢在百忙之中接受我们的采访,下面交给演播室。

好的,感谢前方记者和 map 先生的介绍,我们这就来试试看,怎么使用它来产生乘法口诀的第一列吧!

  1. => (map * [1 1 1 1 1 1 1 1 1] [1 2 3 4 5 6 7 8 9])
  2. (1 2 3 4 5 6 7 8 9)

(额,这个……有点……虽然是成功了吧……)

嗯,手动输入的确有点麻烦啊,没关系,我们还有重复某一元素的函数 repeat 和 可以生成数字序列的 range ~
我们来试试看:

  1. => (repeat 10 3) ;产生 10 3 的序列
  2. (3 3 3 3 3 3 3 3 3 3)
  3. => (repeat 5 "*") ;产生 5 "*" 的序列
  4. ("*" "*" "*" "*" "*")
  5. => (range 10) ;从 0 开始产生不大于 10 的数字序列
  6. (0 1 2 3 4 5 6 7 8 9)
  7. => (range 3 10) ;从 3 开始产生不大于 10 的数字序列
  8. (3 4 5 6 7 8 9)

(这么方便啊。这下好办了。) 
好了,下面我们就改进一下刚才生成第一行乘法口诀的表达式吧~

  1. (map * (repeat 10 1) (range 1 10))
  2. => (1 2 3 4 5 6 7 8 9)

那么第二行怎么生成呢?大家应该能写出来了吧~

  1. (map * (repeat 10 2) (range 1 10))
  2. => (2 4 6 8 10 12 14 16 18)

(嗯哪,只需要把重复 10 个 1,改成重复 10 个 2 就行啦~)
所以,我们把它写成一个函数,这样,我们就可以生成任意的一行了。

  1. (defn multi-table-one-line
  2. [num]
  3. (map * (repeat 10 num) (range 1 10)))

这样以来,第三行就可以这样写了:

  1. => (multi-table-one-line 3)
  2. (3 6 9 12 15 18 21 24 27)

(那,怎么来一次性生成呢?1 2 3 4……这样依次输入太麻烦了。对了!我们再来一次 map!)
哈哈,变聪明了嘛另一个我。(那可不,我可是你。)

  1. => (map multi-table-one-line (range 1 10))
  2. ((1 2 3 4 5 6 7 8 9)
  3. (2 4 6 8 10 12 14 16 18)
  4. (3 6 9 12 15 18 21 24 27)
  5. (4 8 12 16 20 24 28 32 36)
  6. (5 10 15 20 25 30 35 40 45)
  7. (6 12 18 24 30 36 42 48 54)
  8. (7 14 21 28 35 42 49 56 63)
  9. (8 16 24 32 40 48 56 64 72)
  10. (9 18 27 36 45 54 63 72 81))

(这个,和我们平常看到的乘法口诀不一样啊,你看,有一半是重复的可以去掉啊,因为乘法是对称的嘛。)
嗯,对啊,我们仔细想想,第一次的时候,我们并不需要拿 1 1 1 1 1 1 1 1 1 去乘啊,只需一个 1 就可以了,2 的时候 只需两个 2,3 的时候只需三个 3……所以,我们在写 repeat 的时候,应该这么写:(repeat num num),所以生成每行的函数应该改成:

  1. (defn multi-table-one-line
  2. [num]
  3. (map * (repeat num num) (range 1 10)))

(对啊!这样,由于 map 以第一组参数的数量为准,后面多余的就不会再乘了。)

我们来看一下效果:

  1. => (map multi-table-one-line (range 1 10))
  2. ((1)
  3. (2 4)
  4. (3 6 9)
  5. (4 8 12 16)
  6. (5 10 15 20 25)
  7. (6 12 18 24 30 36)
  8. (7 14 21 28 35 42 49)
  9. (8 16 24 32 40 48 56 64)
  10. (9 18 27 36 45 54 63 72 81))

(不错!成功了!但是……平常的乘法口诀,前面都有一个什么 2*3=6 什么的。)

我们生成的乘法口诀可是真正可以用的一组数据啊!不比加上什么 2*3=6 什么的花哨玩意儿实用多了!(喂喂喂。这些数据有毛线用啊。)

好吧,那我们就得把乘法函数改造一番了!乘法函数仅仅只是返回乘法的结果,现在,我们要在乘法的基础上,加上前缀,比如 2*3,本来只返回 6,现在应该返回 2*3=6。这显然是个字符串了,那我们就要使用字符串拼接函数 str,它可以把它接受的参数拼接成一个字符串。

现在我们来写一个“乘法口诀专用乘法函数”:

  1. (defn special-use-multi
  2. [x y]
  3. (str x "乘" y "得" (* x y)))

看看效果:

  1. => (special-use-multi 2 3)
  2. "2乘3得6"

现在我们再来改装我们生成每行的函数:

  1. (defn multi-table-one-line
  2. [num]
  3. (map special-use-multi (repeat num num) (range 1 10)))

看看最后效果:

  1. =>(map multi-table-one-line (range 1 10))
  2. (("1乘1得1")
  3. ("2乘1得2" "2乘2得4")
  4. ("3乘1得3" "3乘2得6" "3乘3得9")
  5. ("4乘1得4" "4乘2得8" "4乘3得12" "4乘4得16")
  6. ("5乘1得5" "5乘2得10" "5乘3得15" "5乘4得20" "5乘5得25")
  7. ("6乘1得6" "6乘2得12" "6乘3得18" "6乘4得24" "6乘5得30" "6乘6得36")
  8. ("7乘1得7" "7乘2得14" "7乘3得21" "7乘4得28" "7乘5得35" "7乘6得42" "7乘7得49")
  9. ("8乘1得8" "8乘2得16" "8乘3得24" "8乘4得32" "8乘5得40" "8乘6得48" "8乘7得56" "8乘8得64")
  10. ("9乘1得9" "9乘2得18" "9乘3得27" "9乘4得36" "9乘5得45" "9乘6得54" "9乘7得63" "9乘8得72" "9乘9得81"))

大功告成啦~可喜可贺~

好啦,我们下次再见~

(别别别别啊!你这怎么有这么多括号啊!)

什么啊,我们这可是一个活的可用的数据啊!比那些死的输入到屏幕上没屁用的东西强一万倍啊!

唉,真难伺候,好吧,我们可以使用 printprintln 函数来输出我们的二维表,也就是两层嵌套的列表,也就是我们的乘法口诀。

首先,我们再来介绍一个高阶函数:

apply (怎么字儿又大了!又要采访了么!)

由于 apply 先生不在家,我们有机会再来采访他!(那你搞这么隆重干啥!)

我们现在认为它可以把列表的括号去掉:

  1. => (print [1 2 3])
  2. [1 2 3] nil
  3. => (apply print [1 2 3])
  4. 1 2 3 nil

好了,我们来完成我们输出二维表的函数:

  1. (defn print-each
  2. "输出每一行" ; 我们可以在函数名和参数之间另起一行,使用字符串作为函数的说明。
  3. [some-coll]
  4. (apply print some-coll)
  5. (println))
  6. (defn print-coll-2d
  7. "输出整体"
  8. [some-coll]
  9. (map print-each some-coll))

print-coll-2d 不仅能输出乘法口诀了,它可以输出任何一个二维的表。
下面我们就进行最后的输出工作了。

  1. => (print-coll-2d (map multi-table-one-line (range 1 10)))
  1. 111
  2. 212 224
  3. 313 326 339
  4. 414 428 4312 4416
  5. 515 5210 5315 5420 5525
  6. 616 6212 6318 6424 6530 6636
  7. 717 7214 7321 7428 7535 7642 7749
  8. 818 8216 8324 8432 8540 8648 8756 8864
  9. 919 9218 9327 9436 9545 9654 9763 9872 9981
  10. (nil nil nil nil nil nil nil nil nil)

这下好了吧~
(嗯……对齐效果不行,而且最后怎么多了一行。)
对齐可以使用格式化输出,最后的是 map 函数返回的 print-each 的值,不可以消除返回值但是……你屁事儿咋这么多呢!下次不叫你了!

最后我们完整的看一下我们的程序:

  1. (defn special-use-multi
  2. "特制乘法"
  3. [x y]
  4. (str x "乘" y "得" (* x y)))
  5. (defn multi-table-one-line
  6. "计算一行的值"
  7. [num]
  8. (map special-use-multi (repeat num num) (range 1 10)))
  9. (defn print-each
  10. "去掉括号输出一行"
  11. [some-coll]
  12. (apply print some-coll)
  13. (println))
  14. (defn print-coll-2d
  15. "输出整体"
  16. [some-coll]
  17. (map print-each some-coll))
  18. ;main
  19. (print-coll-2d (map multi-table-one-line (range 1 10)))