4

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個の辞書検索を実行する必要があります。さらに、各列にはキーと値の両方が含まれ、サイズが2倍になります。

Column簡単なリストを作成し、row_idから配列インデックスへのマッピングへの参照をそのリストに格納できます。利点は、このマッピングを同じ初期テーブルに対応するすべての列で共有でき、一度検索すると、すべての列で機能することです。しかし、これは他の問題を引き起こしますか?

これを行う場合、さらに進んで、実際にマッピングを初期テーブル自体の中に保存できますか?また、オブジェクトからの参照を、Columnそれらが作成された最初のテーブルに戻すことはできますか?機能的なアプローチを想像した方法とは大きく異なるように見えますが、すべてが不変であるため、それがどのような問題を引き起こすのかわかりません。

一般に、機能的アプローチは、引数の1つへの戻り値の参照を維持することに眉をひそめますか?とにかく議論はすでに知られているので、それが何か(最適化や遅延評価など)を壊すようなことはないようです。しかし、多分私は何かが欠けています。

4

1 に答える 1

1

これが私がそれを行う方法です:

  1. フローズンセットからテーブル クラスを派生させます。
  2. 各行は、タプルのサブクラスにする必要があります。

これで、テーブルを変更できなくなりました -> 不変性、すばらしい! 次のステップは、各関数をテーブルに適用して新しいテーブルを生成するミューテーションと見なすことです。

f T -> T'

これは、テーブル T に関数 f を適用して、新しいテーブル T' を生成するものと解釈する必要があります。また、テーブル データの実際の処理をオブジェクト化し、それをテーブルに適用または追加するアクションと見なすこともできます。

add(T, A) -> T'

ここで素晴らしいのは、足し算を引き算にする代わりに、元に戻すモデルを簡単に作成できることです。この考え方に入ると、物事を台無しにする可能性のある状態がないため、コードは非常に簡単に推論できるようになります。

以下は、Python で純粋に機能的な方法でテーブル構造を実装および処理する方法の例です。私見ですが、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)
于 2012-09-25T17:19:54.080 に答える