昔作ったminiadtというパッケージを更新してみた

miniadt

miniadt と言う名前の通り、完全に必要な機能が揃っているというわけではないですが。昔作ったパッケージを更新してみました。 機能は限定的ですが、ある1つのsum typeに対して網羅性チェックのようなものを定義時に行いたいときに使えるかもしれません。

pythonとパターンマッチは本当に相性が悪いので、まともなものを作ることができないのですが。 単純な網羅性チェックだけでも欲しいという場合があったので作ってみました。

使い方

ADTTypeProviderというものを使って型を作成します。

型の定義

from miniadt import ADTTypeProvider

Tree = ADTTypeProvider("Tree")
Node = Tree("Node", "e children")
Leaf = Tree("Leaf", "e")

型の対応関係

Tree型のメンバーとしてNode,Leafがいるというような感じです。 一応、型の対応関係も存在しています。

from miniadt import ADTTypeProvider, ADTType


Tree = ADTTypeProvider("Tree")
Node = Tree("Node", "e children")
Leaf = Tree("Leaf", "e")

# Tree.baseがNode, Leafの親クラス
print(issubclass(Node, Tree.base))  # => True
print(issubclass(Leaf, Tree.base))  # => True

# Tree.base自信はADTTypeのサブクラス
print(issubclass(Node, ADTType))  # => True

# もちろん別の型から作成したものは親子関係にない
XTree = ADTTypeProvider("X")
XNode = XTree("Node", "e children")
XLeaf = XTree("Leaf", "e")

print(issubclass(Node, XTree.base))  # => False
print(issubclass(XLeaf, Tree.base))  # => False

網羅性チェックを行う関数

関数と言ってもクラスで定義します。それにmatch()をデコレータとして使うと網羅性をチェックした関数になります。 網羅性チェックは定義時に行われるだけで、実行時には行われません。 型名と同名のメソッドを呼ぶような実装になっています

@Tree.match
class depth(object):
    def Leaf(e):
        return 1

    def Node(e, children):
        return max(depth(e) for e in children) + 1


print(depth(Leaf(e=10)))  # => 10
print(depth(Node(e=10, children=[Leaf(e=20)])))  # 2

分岐の候補が足りない場合エラーになります。

@Tree.match
class MissingNode(object):
    def Leaf(e):
        return "ok"
# miniadt.NotComprehensive: Node is not found. expected=['Node', 'Leaf']

また、引数の数が型のメンバーの数と一致しない場合にもエラーが出ます。

@Tree.match
class depth(object):
    def Leaf(e, x):  # 1個引数が多い
        return 1

    def Node(e, children):
        return max(depth(e) for e in children) + 1
# miniadt.NotComprehensive: on Tree.Leaf:  expected=['e'] != actual=['e', 'x']

@Tree.match
class depth(object):
    def Leaf(e):
        return 1

    def Node(e):  # 引数が足りない
        return max(depth(e) for e in children) + 1
# miniadt.NotComprehensive: on Tree.Node:  expected=['e', 'children'] != actual=['e']

類縁の機能

match()と似たような機能として以下の2つがあります。

  • match_instance()
  • classify()

両者は共にオブジェクトとして使います。match_instance()はmatch()に引数を追加したい場合に使います。 classify()は、分岐の候補の網羅性チェックだけを行い分配束縛はしないものです。

@Tree.match_instance
class Applicator(object):
    def __init__(self, name):
        self.name = name

    def Leaf(self, e):
        return self.name

    def Node(self, e, children):
        return [self.name, [self(x) for x in children]]

print(Applicator("foo")(Leaf(e=10)))  # => foo
print(Applicator("foo")(Node(e=10, children=[Leaf(e=20)])))  # => ['foo', ['foo']]


@Tree.classify
class ToDict(object):
    def Leaf(self, leaf):
        return leaf.e

    def Node(self, node):
        return {"e": node.e, "children": [self(e) for e in node.children]}

todict = ToDict()
print(todict(Leaf(e=10)))  # => 10
print(todict(Node(e=10, children=[Leaf(e=20)])))  # => {'e': 10, 'children': [20]}