読者です 読者をやめる 読者になる 読者になる

部分適用とその親類とschemaのfieldの作成

python

部分適用とその親類ととschemaのfieldの作成

高階関数はストラテジーパターンの単純系というようなニュアンスで捉えてたりしたのだけれど。 あるいは、1つの用途のものが関数。用途が複数になり始めたものが状態を共有したくなったらオブジェクト 。など。色々オブジェクトと第一級の関数についての話はあったりした。

最近、過去に書いたコードを見て、そう言えば部分適用の親類のような形式でオブジェクトが欲しいというような状況を感じることが合ったことを思い出した。

部分適用及びpartialについて

部分適用というのはpython的な話で言えばfunctools.partialのことで。関数を呼ぶ際の引数の内幾つかを事前に埋めておくというようなときに使われる。関数の呼び出しを2フェイズに分けるというような感じ。2フェイズのみかというとそうではなく。繰り返せば繰り返すだけphaseを分けられる。

from functools import partial


def f(x, y, z):
    return [x, y, z, y, x]

g = partial(f, 1)
h = partial(g, 2)
h(3) # =>[1, 2, 3, 2, 1]

こういうように。もちろん何も渡さずにpartial適用することで無限回関数に引数を埋め込む作業を続ける事ができる。

schemaのfieldの定義と継承について

幾つかの言語ではいわゆるschemaライブラリと言われるものはあると思うし。もちろんpythonにも無数に存在している。schemaライブラリは特に型のゆるい言語においては、渡され処理される値をまとめておくことに使われる。大抵入力値が要求に則しているかどうかを調べる検証用の機能を持っていたりする。

schemaの各属性はフィールドと呼ばれ、それらは大抵型名と同様のクラスとなっている。たとえば整数だけを取るIntegerFieldというものがあったり、正の整数のみを取るPositiveIntegerFieldがあったりする。また、多くのschemaライブラリではフィールドにはrequiredというオプションが用意されていてこれを用いて値が必須か否かを規定したりする。

このschemaのフィールドはクラスの継承で行われている事が多い。例えば以下の様。

from foo import Field
class IntegerField(Field):
    def parse(self, value):
        return int(value)

class PositiveField(IntegerField):
    def validate(self, value):
        return value >= 0

それぞれ継承で行われているのだけれど。結局やることは外部から来た値を内部の自分の望みの形に変換しているというだけ。かつ。自分の要求に則していない値を拒絶するといったことをしている。 これは以下のように考えると1つの巨大な関数で済むのではないかと思ったりした。

関数合成と部分適用と

schemaライブラリについては脇においておいて関数合成の話。 関数合成を型式で考えると推移性を満たした簡略化ということになる。 例えば1つ引数を取って返す関数FとGの関数合成は以下のような感じになる。

F :: a -> b
G :: b -> c

compose :: (a -> b) -> (b -> c) -> a -> c

以前冒頭で書いた関数fについて考えてみると

f :: a -> a -> a -> [a]
g :: a -> a -> [a]
h :: a -> [a]

また1引数だけを部分適用するというように限定して考えた場合について部分適用は以下の様な感じになる。2つの引数を取り、1つは1つ以上の任意個の引数を取りzを返す関数と1つ引数を取り、任意個の引数を取りzを返す関数を返す。

partial:: (a ->  b -*> z) -> a -> (b -*> z)

値を元とする関数をa -> b -> cとしてみた時は、以下の様になる。

partial :: (a -> b -> c) -> a -> (b -> c)

これをcomposeと比べて見た時に。どちらも3つの種類の型を持つ表現同士を組みわせて2つの型数のものに変更している。比べてみた理由は何かといえば、関数合成が2つの何かをまとめて1つの何かを作り上げているということ。これと似たようなイメージが部分適用にも当てはまるのではないかと思ったりした。

Schemaと部分適用

例えば、オブジェクトの最も単純なものが関数だったとして、ストラテジーパターンの最も単純なものが高階関数だったとして。最も単純な継承と言うのは部分適用だったりしないかなというようなことを思ったりした。そして「関数で十分なところにオブジェクトを使うな」という種の話がSchema定義のそれに適用できるんじゃないかなと思ったりした。

まずSchemaをの見方を整えてみよう。単純には変換(convertion)の機能と検証(validation)の機能と2つがあるように思う。当然機能が2つであれば関数に置き換えることはできないので。1つにまとめよう。

例えば、何もしない変換を考えるとそれは受け取ったものをそのまま返す関数であるだろうし。何もしない検証を考えると、受け取ったものに対して何も影響を与えない関数ということになる。

simple_convert :: a -> a
simple_validation :: a -> a

例えば検証について失敗は例外で行うとして、戻り値はそのまま返すとしたら、何も検証しない関数は何も変換しない関数と同様のものと捉える事ができる。これを1つの要素として考えてみることにする。 例えば、IntegerFieldは文字列を受け取ってintを返すfield、PositiveIntegerFieldは入力値を受け取ったらそれが正であるか確認するfieldと呼ぶ事ができる。

int :: a -> int
positive :: a -> a{error if not a > 0}

IntegerField :: a -> Field(int)
PositiveField :: a -> Field(int){error if not a > 0}

これで考えてみると以下のようなmap関数があればcomposeでPositiveFieldが作れるということになる。

map :: (a -> b) -> (Field(a) -> Field(b))
to_field :: (a -> b) -> (a -> Field(b))

PositiveField = compose(to_field(int), map(positive))

to_field(int) :: a -> Field(int)
map(positive) :: Field(int) -> Field(int)
compose :: (a -> b) -> (b -> c) -> a -> c

PositiveField :: a -> Field(int)

それじゃrequiredなどはどうするかと言う話もでてきて。これはそもそも。値を持つ辞書のようなものがあればそれで済む。つまり。IntegerFieldはField(int)ではなくField(env, int)。 ここでenv -> env'となるようなものは何かと言うと、requiredの挿入ということになる。

to_required :: env -> env'{insert required=True}
to_optional :: env -> env'{insert required=False}

2つを組み合わせよう。

to_field :: (a -> b) -> a -> Field(env, b)
modify :: env -> env'

map :: (a -> b) -> (Field(env,a) -> Field(env, b))
merged :: (Field(env, a) -> Field(env', a) -*> Field(env'', a))  -> env -> (Field(env', a) -*> Field(env'', a))

まだ2種類の機能が必要。そもそもcomposeもenvの中に含められないか。というと何かコンテナに入れれば良いのではないかと思う。例えば(compose(f, g))(val)というのはexecute([f, g], value)と考えていってしまっても良いし。compose(compose(f, g), h)はexecute([f, g, h], value)と成り得るexecuteを定義すれば良いのかなと。

すると全部は以下の形でまとめられる。

merged :: Field(env) -> Field(env') -*> Field(env'')

Fieldの変更Fieldの生成

全部が1つの形にまとめられたわけだけれど。あれははじめからField(env)の値があることが前提になっている。すでに作成済みのFieldオブジェクトにあれこれ設定を加えられると言う意味でしか無かった。

ところでFieldを部分適用するといった場合には、実質的には、最後に実行されるFieldは適用されるまで遅延されている。たとえばpartial(f,x)についてfは適用されないまま別の関数gが変える。

つまりto_fieldが以下のような式だったので

to_field :: a -> Field(env)

挿入ということは値の充足なわけでこれは部分適用なのではないかな。 ということはField(env) -> Field(env')の書き方は本当は嘘なのかもしれない。

to_field :: a -> Field(env)
to_field' = compose(to_field, x) :: b -> Field(env)
to_field'' = compose(to_field', y) :: c -> Field(env)
to_field''' = compose(to_field'', z) :: d -> Field(env)

インスタンス化は何かというとd -> Field(env)にdを渡す適用そのもの。そもそもクラスと継承で作ったとしても最後に必ずコンストラクタの呼び出しが来る。これを最後にFieldを適用するというように考えるとしたらというようなことを考えていたらしい。

追記: どこに挿入するかというのがひどく曖昧で辛い。goto的な大域脱出が必要

gistf0eb0dc1cccb4e963b49