[英]Converting an imperative algorithm that “grows” a table into pure functions
我的程序是用Python 3编写的,它有很多地方以一个(非常大的)类似数字的数据结构开始,并按照某种算法为它添加列。 (每个地方的算法都不同。)
我试图将其转换为纯函数方法,因为我遇到了命令式方法的问题(难以重用,难以回忆临时步骤,难以实现“懒惰”计算,由于依赖于状态而容易出错等) 。
Table
类实现为字典字典:外部字典包含行,由row_id
索引; inner包含一行中的值,由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
对象的引用放回到创建它们的初始表吗? 它似乎与我想象的功能性工作方式有很大不同,但我看不出它会导致什么问题,因为一切都是不可改变的。
通常,函数方法是否会将返回值中的引用保留为其中一个参数? 它似乎不会破坏任何东西(如优化或懒惰评估),因为无论如何已经知道了这个论点。 但也许我错过了一些东西。
我将如何做到这一点:
现在你无法修改表 - > immutability,太棒了! 下一步可能是将每个函数都考虑为应用于表的突变以生成新的函数:
f T -> T'
这应该被理解为应用表T上的函数f来产生新的表T'。 您还可以尝试对表数据的实际处理进行客观化,并将其视为您应用或添加到表中的Action。
add(T, A) -> T'
这里的好处是可以减去add而不是为您提供一种简单的撤消建模方法。 当你进入这种心态时,你的代码变得非常容易推理,因为你没有可以搞砸的状态。
下面是一个如何在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)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.