[英]understanding cyclic/recursive data types in haskell
我试图完全理解 haskell 的数据类型,所以我创建了这些:
data District = District {nameOfCity :: String,
subDistricts ::[DistrictUnit]}
deriving (Show, Eq)
data DistrictUnit = DistrictUnit District
| BranchUnit Branch
deriving (Show, Eq)
data Branch = Branch {nameOfBranch :: String,
numOfEmployees :: Int,
sales :: Int}
deriving (Show, Eq)
这里有些例子:
a = District {nameOfCity = "Berlin", subDistrcits = []}
b = District {nameOfCity = "New York",
subDistricts =
[DistrictUnit
(District {nameOfCity = "Amsterdam",
subDistricts =
[DistrictUnit
(District {nameOfCity = "London",
subDistricts = []})]})]}
c = District {nameOfCity = "Iowa" ,
subDistricts =
[BranchUnit
(Branch {nameOfBranch = "Omaha",
numOfEmployees = 3,
sales = 2343})]}
现在我正在尝试构建两个函数:getNameOfCity 和 getNameOfBranch:
getNameOfCity b -> ["New York", "Amsterdam", "London"]
但我有大量的模式匹配问题。 我只是不知道如何构建适用于所有输入的 function。
这是我的尝试:
getNameOfCity :: District -> [String]
getNameOfCity (District a b) = [a]
这个 function 只会给我名字:
["New York"]
所以我尝试了这个:
getNameOfCity (District a [DistrictUnit (District b c)]) = [a, b]
而且(当然)那个人只会给我前两个名字:
["New York", "Amsterdam"]
如何使 getNameOfCity 适用于所有输入? getNamesOfBranch 是否会以同样的方式工作(因为它有不同的参数)? 先感谢您:)
与其尝试使用单个 function 做所有事情,不如编写单独的帮助程序更容易:
allDistrictNames :: District -> [String] -- This was your `getNameOfCity`
allDistrictNames_u :: DistrictUnit -> [String]
allDistrictNames_ul :: [DistrictUnit] -> [String]
allDistrictNames
和allDistrictNames_u
现在可以通过简单的模式匹配和相互调用来实现。 allDistrictNames_ul
需要对所有列表元素调用allDistrictNames_u
,并合并结果。 有一个标准的 function 用于这个确切的目的。
您希望getNameOfCity
应用于District
以返回其nameOfCity
字段,该字段位于其每个 subDistricts 的城市名称列表之前,这些subDistricts
是District
s 而不是Branch
es。
所以你有一个很好的起点,如下所示:
getNameOfCity :: District -> [String]
getNameOfCity (District name units) = [name]
但是当然你需要对units
做一些事情。 您在第二个示例中编写的模式[DistrictUnit (District bc)]
仅在subDistricts
字段包含恰好一个元素的列表时匹配,其中该元素也是DistrictUnit
,并且仅从中提取nameOfCity
字段( b
)区,忽略任何进一步的嵌套分区(在c
中)。
为了帮助您解决这个问题,我将为您提供一个模板供您填写,遵循一个常见模式,这在您熟悉 Haskell 时非常有用:从期望的结果返回,将问题分解为小问题步骤,并给每个步骤一个名称和类型签名。 这样,如果您犯了错误,编译器将生成更易于理解的错误消息。 然后,一旦有了可行的解决方案,就可以删除中间变量并将步骤合并为更紧凑的表达式。
首先,您想要的结果是附加在所有分区名称列表中的根区名称。
getNameOfCity :: District -> [String]
getNameOfCity (District name units) = let
allCities :: [String]
allCities = …
in name : allCities
要构建它,您需要遍历units
并从每个区域中提取名称,然后连接所有结果。
getNameOfCity :: District -> [String]
getNameOfCity (District name units) = let
districtCities :: [[String]]
districtCities = … -- List of list of city names for each ‘District’
allCities :: [String]
allCities = … -- Concatenated ‘districtCities’
in name : allCities
可以通过几种方式对单元进行迭代,但我要做的是首先仅提取District
s,这样您就可以使用更窄、更精确的类型,然后您可以简单地递归地应用getNameOfCity
。
getNameOfCity :: District -> [String]
getNameOfCity (District name units) = let
-- Extract only those ‘units’ that are ‘District’s
districts :: [District]
districts = …
-- Hint:
-- Hoogle ‘(a -> Maybe b) -> [a] -> [b]’
-- and define ‘getDistrict :: DistrictUnit -> Maybe District’;
-- or use a list comprehension of the form: ‘[… | … <- units]’
districtCities :: [[String]]
districtCities = …
allCities :: [String]
allCities = …
in name : allCities
您可以对分支名称采用非常相似的方法; 这是一个很好的练习,一旦你把它们都放在你面前,考虑如何抽象出它们的共性。
这也是使用Semigroup
和Monoid
类型类解决此类问题的常见高级方法,它们是可以以某种方式关联地“附加”的类型集的数学名称( <>
运算符),并且具有默认的“空”值( mempty
),它是用于追加的无操作。 一般的方法是将map
每个值转换为一个“摘要”值——这里是城市名称列表——然后将这些摘要fold
在一起,方便地组合成一个称为foldMap
的操作。 例如:
districtCities :: District -> [String]
districtCities (District city units) = [city] <> foldMap unitCities units
unitCities :: DistrictUnit -> [String]
-- Recursively summarise district.
unitCities (DistrictUnit district) = districtCities district
-- Return “empty” value for branch.
unitCities (BranchUnit branch) = mempty
这概括为累积其他字段,例如分支名称:
districtBranches :: District -> [String]
districtBranches (District _ units) = foldMap unitBranches units
unitBranches :: DistrictUnit -> [String]
unitBranches (DistrictUnit district) = districtBranches district
unitBranches (BranchUnit branch) = [nameOfBranch branch]
或其他类型的值,如总销售额,使用Sum
幺半群:
import Data.Monoid (Sum(..))
totalSales :: District -> Int
totalSales district = getSum (districtSales district)
-- Or: totalSales = getSum . districtSales
districtSales :: District -> Sum Int
districtSales (District _ units) = foldMap unitSales units
unitSales :: DistrictUnit -> Sum Int
unitSales (DistrictUnit district) = foldMap districtSales district
unitSales (BranchUnit branch) = Sum (branchSales branch)
甚至一次使用多个值,例如: (Sum 1, ["London"]) <> (Sum 2, ["Paris"])
= (Sum 3, ["London", "Paris"])
。 不过,请注意所有这些实现如何也具有相似的结构,并考虑如何对这种“折叠”模式进行一般抽象。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.