我一直在撰写一系列关于用不同编程语言解决一个有趣、小型且有些不寻常的问题的文章(Groovy、Python 和 Java 到目前为止)。
简而言之,问题是如何将大宗物资拆分成单位(例如,将 10 包一磅装的您最喜欢的咖啡分开),然后重新包装成价值相近的篮子,分发给社区中挣扎的邻居。
我已经探索过的三种解决方案都构建了采购的大宗包装数量列表。我通过在 Groovy 中使用 map,在 Python 中使用字典,以及在 Java 中将元组实现为实用程序类来完成此操作。我使用了每种语言的列表处理功能,将大宗包装拆分成其组成部分的列表,我分别使用 map、字典和元组对其进行建模。我需要采用迭代方法将单位从列表移动到篮子中;这种迭代方法在不同的语言中非常相似,细微的差别在于我可以在 Groovy 和 Java 中使用 for {...}
循环,而在 Python 中需要 while…:
。但总而言之,它们使用了非常相似的解决方案,其中零星地带有函数式编程的暗示和封装在对象中的行为。
认识 Julia
在本文中,我将用 Julia 探索相同的问题,这(除其他外)意味着抛开我习惯的面向对象和函数式编程范式。我难以适应非面向对象的语言。我从 1997 年左右开始使用 Java 编程,从 2008 年左右开始使用 Groovy,所以我习惯于将数据和行为捆绑在一起。除了通常喜欢方法调用挂在对象或有时是类上的代码外观外,我真的喜欢类文档将类处理的数据及其处理方式打包在一起的方式。这现在对我来说似乎如此“自然”,以至于学习一种文档分别描述类型和函数的语言对我来说似乎很困难。
说到学习一门语言,我还是 Julia 的新手。我喜欢它面向我通常需要解决的那些类型的问题(例如,数据、计算、结果)。我喜欢对速度的追求。我喜欢将 Julia 设计成一种可以使用模块化和迭代方法解决复杂问题的语言的决定。我喜欢使优秀的现有分析库可用的想法。但是,我对非面向对象的设计仍然持观望态度。我似乎也更频繁地在我的 Groovy 和 Java 编程中使用函数式方法,所以我想我可能会在 Julia 中怀念这一点。
但够了猜测,让我们编写一些代码吧!
Julia 解决方案
我的第一个决定是如何实现数据模型。Julia 支持复合类型,看起来类似于 C 中的 struct
,Julia 甚至使用关键字 struct
。值得注意的是,struct
是不可变的(除非声明为 mutable struct
),这对于这个问题来说很好,因为数据不需要被修改。
通过遵循我在 Java 解决方案中采用的方法,可以将 Unit struct
定义为:
struct Unit
item::String
brand::String
price::Int
end
类似地,Pack
被定义为 Unit
实例的大宗包装
struct Pack
unit::Unit
count::Int
Pack(item, brand, unitCount,p ackPrice) =
new(Unit(item, brand, div(packPrice,unitCount)), unitCount)
end
这里有一个有趣的事情:Julia “内部构造函数”。在 Java 解决方案中,我认为大宗包装内部的单位(至少在我的想法中)是大宗包装的一部分,而不是外部可见的东西,所以我决定传入商品、品牌、单位数量和包装价格,并让 Pack
对象在内部创建其单位。我在这里也会做同样的事情。
因为 Julia 不是面向对象的,所以我无法向 Pack
添加方法来给出单位价格与包装价格,或者将其解包为 Unit
实例列表。我可以声明“getter”函数来完成相同的任务。(我可能不需要这些,但我无论如何都要这样做,看看 Julia 方法是如何工作的)
item(pack::Pack) = pack.unit.item
brand(pack::Pack) = pack.unit.brand
unitPrice(pack::Pack) = pack.unit.price
unitCount(pack::Pack) = pack.count
packPrice(pack::Pack) = pack.unit.price * pack.count
unpack(pack::Pack) = Iterators.collect(Iterators.repeated(pack.unit,pack.count))
unpack()
方法与我在 Java 类 Pack
中声明的同名方法非常相似。函数 Iterators.repeated(thing,N)
创建一个迭代器,它将传递 N
个 thing
副本。Iterators.collect
(iterator
) 函数处理 iterator
以生成一个由其传递的元素组成的数组。
最后,是 Bought struct
struct Bought
pack::Pack
count::Int
end
unpack(bought::Bought) =
Iterators.collect(Iterators.flatten(Iterators.repeated(unpack(bought.pack),
bought.count)))
再一次,我创建了一个已解包的 Pack
实例数组的数组(即单位),并使用 Iterators.flatten()
将其转换为一个简单的数组。
现在我可以构建我购买的物品的列表了
packs = [
Bought(Pack("Rice","Best Family",10,5650),1),
Bought(Pack("Spaghetti","Best Family",1,327),10),
Bought(Pack("Sardines","Fresh Caught",3,2727),3),
Bought(Pack("Chickpeas","Southern Style",2,2600),5),
Bought(Pack("Lentils","Southern Style",2,2378),5),
Bought(Pack("Vegetable oil","Crafco",12,10020),1),
Bought(Pack("UHT milk","Atlantic",6,4560),2),
Bought(Pack("Flour","Neighbor Mills",10,5200),1),
Bought(Pack("Tomato sauce","Best Family",1,190),10),
Bought(Pack("Sugar","Good Price",1,565),10),
Bought(Pack("Tea","Superior",5,2720),2),
Bought(Pack("Coffee","Colombia Select",2,4180),5),
Bought(Pack("Tofu","Gourmet Choice",1,1580),10),
Bought(Pack("Bleach","Blanchite",5,3550),2),
Bought(Pack("Soap","Sunny Day",6,1794),2)]
我开始看到一种模式……这看起来非常像这个问题的 Java 解决方案。就像那时一样,这表明我购买了一包 Best Family Rice,其中包含 10 个单位,价格为 5650(使用那些疯狂的货币单位,就像在其他示例中一样)。我购买了一包 10 袋装的大米,并且我购买了 10 包每包一袋的意大利面。
有了我购买的包装列表,我现在可以解包成单位,然后再处理重新分配的问题
units = Iterators.collect(Iterators.flatten(unpack.(packs)))
这里发生了什么?好吧,像 unpack.(packs)
这样的构造——即函数名和参数列表之间的点——将函数 unpack()
应用于列表 packs
中的每个元素。这将生成一个列表的列表,对应于我购买的已解包的 Packs
组。为了将其转换为单位的平面列表,我应用 Iterators.flatten()
。因为 Iterators.flatten()
是惰性的,为了使平面化发生,我将其包装在 Iterators.collect()
中。这种函数组合符合函数式编程的精神,即使您没有看到函数像在 JavaScript、Java 或其他语言中以函数式方式编写程序的程序员所熟悉的那样链接在一起。
一个观察结果是,这里创建的单位列表实际上是一个索引从 1 而不是 0 开始的数组。
有了 units 作为购买和解包的单位列表,我现在可以着手将它们重新包装到篮子中。
这是代码,它与 Groovy、Python 和 Java 版本没有特别大的不同
1 valueIdeal = 5000
2 valueMax = round(valueIdeal * 1.1)
3 hamperNumber = 0
4 while length(units) > 0
5 global hamperNumber += 1
6 hamper = Unit[]
7 value = 0
8 canAdd = true
9 while canAdd
10 u = rand(0:(length(units)-1))
11 canAdd = false
12 for o = 0:(length(units)-1)
13 uo = (u + o) % length(units) + 1
14 unit = units[uo]
15 if length(units) < 3 || findfirst(u -> u == unit,hamper) === nothing && (value + unit.price) < valueMax
16 push!(hamper,unit)
17 value += unit.price
18 deleteat!(units,uo)
19 canAdd = length(units) > 0
20 break
21 end
22 end
23 end
24 Printf.@printf("\nHamper %d value %d:\n",hamperNumber,value)
25 for unit in hamper
26 Printf.@printf("%-25s%-25s%7d\n",unit.item,unit.brand,unit.price)
27 end
28 Printf.@printf("Remaining units %d\n",length(units))
29 end
一些说明,按行号
- 第 1-3 行:设置要加载到任何给定篮子中的理想值和最大值,并初始化 Groovy 的随机数生成器和篮子编号
- 第 4-29 行:只要有更多可用单位,此
while
循环就会将单位重新分配到篮子中 - 第 5-7 行:递增(全局)篮子编号,获取一个新的空篮子(
Unit
实例数组),并将其值设置为 0 - 第 8 行和 9-23 行:只要我可以向篮子中添加单位…
- 第 10 行:获取一个介于零和剩余单位数减 1 之间的随机数
- 第 11 行:假设我找不到更多要添加的单位
- 第 12-22 行:此
for
循环,从随机选择的索引开始,将尝试查找可以添加到篮子中的单位 - 第 13-14 行:弄清楚要查看哪个单位(记住数组从索引 1 开始)并获取它
- 第 15-21 行:如果只剩下几个单位,或者如果添加该单位后篮子的价值不太高,并且该单位尚未在篮子中,我可以将此单位添加到篮子中
- 第 16-18 行:将单位添加到篮子中,将篮子价值增加单位价格,并从可用单位列表中删除该单位
- 第 19-20 行:只要还有单位剩下,我就可以添加更多,因此跳出此循环以继续查找
- 第 22 行:在此
for
循环退出时,如果我已经检查了每个剩余单位但找不到一个可以添加到篮子中的单位,则篮子已完成;否则,我找到了一个并且可以继续寻找更多 - 第 23 行:在此
while
循环退出时,篮子已尽可能装满,因此… - 第 24-28 行:打印出篮子的内容和剩余单位信息
- 第 29 行:当我退出此循环时,没有更多单位剩下
运行此代码的输出看起来与其他程序的输出非常相似
Hamper 1 value 5020:
Tea Superior 544
Sugar Good Price 565
Soap Sunny Day 299
Chickpeas Southern Style 1300
Flour Neighbor Mills 520
Rice Best Family 565
Spaghetti Best Family 327
Bleach Blanchite 710
Tomato sauce Best Family 190
Remaining units 146
Hamper 2 value 5314:
Flour Neighbor Mills 520
Sugar Good Price 565
Vegetable oil Crafco 835
Coffee Colombia Select 2090
UHT milk Atlantic 760
Tea Superior 544
Remaining units 140
Hamper 3 value 5298:
Tomato sauce Best Family 190
Tofu Gourmet Choice 1580
Sugar Good Price 565
Bleach Blanchite 710
Tea Superior 544
Lentils Southern Style 1189
Flour Neighbor Mills 520
Remaining units 133
…
Hamper 23 value 4624:
Chickpeas Southern Style 1300
Vegetable oil Crafco 835
Tofu Gourmet Choice 1580
Sardines Fresh Caught 909
Remaining units 4
Hamper 24 value 5015:
Tofu Gourmet Choice 1580
Chickpeas Southern Style 1300
Chickpeas Southern Style 1300
Vegetable oil Crafco 835
Remaining units 0
最后一个篮子的内容和价值被缩写了。
结束语
再一次,随机数驱动的列表操作似乎使程序的“工作代码”部分与 Groovy、Python 和 Java 版本非常相似。令我高兴的是,我在 Julia 中找到了良好的函数式编程支持,至少在解决这个小问题所需的简单列表处理方面是如此。
鉴于主要工作围绕 for
和 while
循环展开,在 Julia 中,我看不到任何类似于
for (boolean canAdd = true; canAdd; ) { … }
这意味着我必须在 while
循环外部声明 canAdd
变量。这太糟糕了——但也不是什么可怕的事情。
我确实怀念无法将行为直接附加到我的数据上,但这只是我个人对面向对象编程的欣赏。但这肯定不是这个程序中的巨大障碍;但是,与一位友善的作者就我的 Java 版本进行的交流让我意识到,我应该构建一个类来完全封装分发函数,使其类似于篮子列表,主程序只需打印出来即可。这种方法在像 Julia 这样的非面向对象语言中是不可行的。
优点:低仪式感,√;体面的列表处理,√;简洁易读的代码,√。总而言之,这是一次愉快的体验,支持了 Julia 可以成为解决“普通问题”和作为脚本语言的不错选择的观点。
下次,我将用 Go 完成这个练习。
评论已关闭。