将"grows"表的命令式算法转换为纯函数



我的程序是用Python 3编写的,在许多地方,它以一个(非常大的)类似表的数字数据结构开始,并按照特定的算法向其中添加列。(每个地方的算法都不一样。)

我正试图将其转换为纯功能方法,因为我遇到了命令式方法的问题(难以重用,难以记忆临时步骤,难以实现"懒惰"计算,由于依赖状态而容易出现错误,等等)

Table类实现为字典的字典:外部字典包含由row_id索引的行;内部包含由column_title索引的行内的值。该表的方法非常简单:

# return the value at the specified row_id, column_title
get_value(self, row_id, column_title)
# return the inner dictionary representing row given by row_id
get_row(self, row_id) 
# add a column new_column_title, defined by func
# func signature must be: take a row and return a value
add_column(self, new_column_title, func)

到目前为止,我只是简单地将列添加到原始表中,每个函数都将整个表作为一个参数。当我转向纯函数时,我必须使所有参数都是不可变的。因此,初始表变为不可变的。任何附加列都将被创建为独立列,并只传递给那些需要它们的函数。一个典型的函数将获取初始表和一些已经创建的列,并返回一个新列。

我遇到的问题是如何实现独立列(Column)?

我可以给他们每人做一本字典,但看起来很贵。事实上,如果我需要对每个逻辑行中的10个字段执行操作,那么我需要进行10次字典查找。除此之外,每列都将包含键和值,使其大小加倍。

我可以使Column成为一个简单的列表,并在其中存储从row_id到数组索引的映射的引用。其好处是,这种映射可以在对应于同一初始表的所有列之间共享,而且一旦查找一次,它就适用于所有列。但这会造成其他问题吗?

如果我这样做,我是否可以更进一步,将映射实际存储在初始表本身中?我可以将Column对象的引用放回创建它们的初始表吗?这似乎与我想象的功能性工作方法大不相同,但我看不出它会引起什么问题,因为一切都是不可变的。

一般来说,函数方法是否不赞成在返回值中保留对其中一个参数的引用?它似乎不会破坏任何东西(比如优化或懒惰评估),因为无论如何,这个论点都是已知的。但也许我错过了什么。

以下是我的操作方法:

  1. 从frozenset派生表类
  2. 每一行都应该是元组的一个子类

现在你不能修改表->不变性,太棒了!下一步可以将每个函数视为一个突变,将其应用于表生成一个新的:

f T -> T'

这应该被理解为应用表T上的函数f来产生一个新的表T’。您也可以尝试将表数据,并将其视为应用或添加到桌子

add(T, A) -> T'

这里最棒的是加法可以是减法,而不是给你一种简单的撤消建模方法。当你进入这种心态时,你的代码变得很容易推理,因为你没有一个州可以把事情搞砸了。

下面是一个如何实现和处理表的示例在Python中以纯函数的方式构造。Imho,Python不是学习FP的最佳语言,因为它使程序的强制性。我认为Haskell、F#或Erlang是更好的选择。

class Table(frozenset):
def __new__(cls, names, rows):
return frozenset.__new__(cls, rows)
def __init__(self, names, rows):
frozenset.__init__(self, rows)
self.names = names
def add_column(rows, func):
return [row + (func(row, idx),) for (idx, row) in enumerate(rows)]
def table_process(t, (name, func)):
return Table(
t.names + (name,),
add_column(t, lambda row, idx: func(row))
)
def table_filter(t, (name, func)):
names = t.names
idx = names.index(name)
return Table(
names,
[row for row in t if func(row[idx])]
)
def table_rank(t, name):
names = t.names
idx = names.index(name)
rows = sorted(t, key = lambda row: row[idx])
return Table(
names + ('rank',),
add_column(rows, lambda row, idx: idx)
)
def table_print(t):
format_row = lambda r: ' '.join('%15s' % c for c in r)
print format_row(t.names)
print 'n'.join(format_row(row) for row in t)
if __name__ == '__main__':
from random import randint
cols = ('c1', 'c2', 'c3')
T = Table(
cols,
[tuple(randint(0, 9) for x in cols) for x in range(10)]
)
table_print(T)
# Columns to add to the table, this is a perfect fit for a
# reduce. I'd honestly use a boring for loop instead, but reduce
# is a perfect example for how in FP data and code "becomes one."
# In fact, this whole program could have been written as just one
# big reduce.
actions = [
('max', max),
('min', min),
('sum', sum),
('avg', lambda r: sum(r) / float(len(r)))
]
T = reduce(table_process, actions, T)
table_print(T)
# Ranking is different because it requires an ordering, which a
# table does not have.
T2 = table_rank(T, 'sum')
table_print(T2)
# Simple where filter: select * from T2 where c2 < 5.
T3 = table_filter(T2, ('c2', lambda c: c < 5))
table_print(T3)

最新更新