Elm 的类型支持泛型,泛型通常能让接口更灵活。例如,类型Maybe a可以存放任意类型的值。
type Maybe a = Nothing | Just a
注意,在上面的例子中,类型参数a出现在等号=的两边。我们说a绑定到了类型定义中的某些数据。
不过在某些情况下,类型参数只出现在等式的左边。
type Distance unit = Distance Float
在上面Distance的定义中,unit是一个自由类型参数,没有绑定到类型中的任何数据。这样的类型也称为幻影类型。
当我们想在编译期强制施加约束时,它出奇地有用。例如,我们想确保只把相同单位的距离相加。
-- Distance is an opaque type, since the module does not expose its variants.
-- This means that users may only use the functions meter, foot and add to manipulate distances.
module Distance exposing (Distance, Meter, Foot, meter, foot, add)
-- The Distance type has a phantom type 'unit'.
type Distance unit = Distance Float
-- We define two types that will be used in place
-- of the phantom type 'unit' in our constructor functions.
type Meter = Meter
type Foot = Foot
-- Constructor for Meter
meter : Distance Meter
meter = Distance 1.0
-- Constructor for Foot
foot : Distance Foot
foot = Distance 0.3048
-- The add function cannot take two parameters of different types.
-- So we cannot add meters and feet by mistake.
add : Distance unit -> Distance unit -> Distance unit
add (Distance d1) (Distance d2) = Distance (d1 + d2)
该模块之外的代码无法访问Distance类型的内部。用户可以相加两个Distance Meter,但不能把一个Distance Meter和一个Distance Foot相加。
import Distance exposing (Distance)
-- Compiles
twoMeters = Distance.add Distance.meter Distance.meter
-- Does not compile
errDist = Distance.add Distance.meter Distance.foot
哪些类型能作为幻影类型并没有限制,记录类型也可以。用作幻影类型时,记录能够表达复杂的约束,而且这些约束可以随函数变化。
我们在Distance模块里新增一个距离单位,叫作LegoBlock。实体积木作为受管制的对象,总是具有两个性质:它们的距离是非分数的,也是非负的。
type LegoBlock = LegoBlock
fourStuds : Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
fourStuds = Distance 4.0
nonFractional和nonNegative是一个记录的字段,而这个记录只存在于类型参数里,在类型参数之外并不存在,所以很适合给它们赋予()类型,也就是 单元类型,它除了表明自身存在之外不携带任何信息。
当然,得到任意的LegoBlock距离也是可以的,例如在算出距离的差值或比值之后。
negativeStud : Distance { properties | unit: LegoBlock, nonFractional : () }
negativeStud = Distance -1.0
threeFiddyStud : Distance { properties | unit: LegoBlock, nonNegative : () }
threeFiddyStud = Distance 3.50
crazyStud : Distance { properties | unit: LegoBlock }
crazyStud = Distance -13.37
上面这些值都是合法的,也都能通过编译。但我们特别关注像fourStuds这样同时具有nonFractional和nonNegative属性的值,因为它们代表的是能够与下面这个函数组合的实体积木。
combineLegoBlocks
: Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
combineLegoBlocks = add
我们再添加一些函数,让用户可以创建和完善LegoBlock距离。
newLegoBlock : Float -> Distance { properties | unit: LegoBlock }
newLegoBlock dist = Distance dist
floorDistance : Distance properties -> Distance { properties | nonFractional : () }
floorDistance (Distance dist) = Distance (toFloat (floor dist))
ceilingDistance : Distance properties -> Distance { properties | nonFractional : () }
ceilingDistance (Distance dist) = Distance (toFloat (ceiling dist))
absDistance : Distance properties -> Distance { properties | nonNegative : () }
absDistance (Distance dist) = Distance (abs dist)
注意,floorDistance、ceilingDistance和absDistance也能处理LegoBlock以外的单位,而且它们通常不对输入的属性作任何假设,只是保证输出具有某个特定的属性,要么是nonFractional,要么是nonNegative。
我们来看看一些结果。
import Distance exposing (Distance)
-- Compiles
distance1 = combineLegoBlocks fourStuds fourStuds
-- Does not compile
distance2 = combineLegoBlocks fourStuds threeFiddyStud
-- Compiles
distance3 = combineLegoBlocks fourStuds (floorDistance threeFiddyStud)
-- Does not compile
distance4 = combineLegoBlocks fourStuds negativeStud
-- Compiles
distance5 = combineLegoBlocks fourStuds (absDistance negativeStud)
-- Does not compile
distance6 = combineLegoBlocks fourStuds crazyStud
-- Compiles
distance7 = combineLegoBlocks fourStuds (floorDistance (absDistance crazyStud))
-- Compiles
distance8 = combineLegoBlocks fourStuds (ceilingDistance (absDistance crazyStud))
一般来说,floorDistance和ceilingDistance会给出不同的结果,但提供相同的保证。这正是幻影类型技巧的强大之处:在为用户提供灵活选择的同时,保持强有力的保证。
你,邪恶之主,为自己遍布世界各地的邪恶地牢里的TreasureChest的质量深感自豪。
要保持这样的质量并不容易,你的地牢管理员总是把宝物制作搞砸,于是你决定提供一个 Elm API,让他们服从你的意志。
有两个条件你绝不会让步:
TreasureChest必须由至少 8 个字符的安全密码保护TreasureChest都应装有一件独一无二的宝物你的地牢管理员会提出一组密码和宝物的建议,只有合适的TreasureChest才会被创建。
在这些条件下,从一组建议中创建安全宝箱有两种可能的方式:
这两种方式可能给出不同的结果(例如对于[("strong_password", GoldStatue), ("1234", GoldStatue)]),但你并不介意用哪种,所以你打算让地牢管理员自己决定。
一个既能给用户留些选择,又能保证最终结果的某些性质的 API? 听起来简直是为幻影类型技术量身定做的!
TreasureChest类型和它配套的getTreasure已经给出,但你需要设计一个表示宝箱建议的类型。
实现Chest类型,实现makeChest,并修正secureChest和uniqueTreasures的类型签名。
makeChest和makeTreasureChest的类型签名已作为需求的一部分提供,不要修改它们。
Chest应持有与TreasureChest相同的数据,并应有两个类型实参:一个用于treasure,另一个是用于conditions的记录幻影类型。
注意,由于Chest使用了幻影类型,因此它应当是不透明的,以防在TreasureFactory模块之外的任何地方使用它。
在这个练习中,它甚至完全没有被暴露出来。
修改secureChest和uniqueTreasures的类型签名,用可扩展记录作为幻影类型来添加约束。
secureChest应接收一个没有特定条件的Chest,返回一个Maybe Chest,并在其幻影记录中带上额外的条件securePassword : ()。
uniqueTreasures应接收一个没有特定条件的List Chest,返回一个List Chest,并在其幻影记录中添加额外的条件uniqueTreasure : ()。
Elm 测试可以访问已暴露的函数,但无法访问类型签名,因此测试无法验证你使用的签名是否正确。 当然,最能说明你的签名正确的,就是模块和测试能够成功编译并运行,不过我们已经创建了一条分析器规则,在你提交解答后会检查你的类型签名。
宝箱准备好之后,你需要挑出安全的那些。
实现secureChest,它只对密码长度至少为 8 个字符的宝箱返回Just变体。
满足该条件的Chest应在其幻影类型中添加额外的securePassword : ()条件。
只有最稀有的宝物才配得上放进地牢,哪怕多出一件,也会让宝物显得廉价。
实现uniqueTreasures,它接收一个由Chest组成的数组,并返回一个数组,里面是在输入数组中宝物独一无二的那些Chest。
如果一件宝物在输入中出现两次,它就不应出现在输出中。
输入的Chest不应带有特定条件,输出的Chest则应添加额外的条件uniqueTreasure : ()。
好好享受这些最棒的TreasureChest吧,它们一定会像蜜糖吸引苍蝇一样把冒险者吸引过来。
实现makeTreasureChest,它接收一个既安全又独一无二的Chest,并创建一个TreasureChest。
由于TreasureChest是不透明类型,这将是创建它的唯一方式,再糟糕的地牢管理员也没法把它搞砸。