1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コードを触らず機能追加:eml演算子で作る拡張可能な計算機能

1
Posted at

はじめに

今回が初投稿になります。温かい目で見守ってください。

eml演算子について

eml演算子とは、今年の4月に投稿されたAndrzej Odrzywołek氏による論文”All elementary functions from a single binary operator”によって提唱されている、初等関数の最小素子です。
私も詳しく読んだわけではないのですが、なんと以下の関数によって、全ての初等関数を表現可能という、実用的かはわかりませんが画期的なものです。

eml(x,y)=e^x-ln(y)

今回やったこと

今回はこれを使って、eml演算子だけを使った計算の実装をしました。
何が嬉しいのかは、後述します。

構成

今回は以下のクラス、関数を実装しました。

1. eml(x,y)
言わずもがな、eml演算子を計算する関数です。

execute_eml.py
import numpy as np

def eml(x, y):
    x = np.asarray(x)
    y = np.asarray(y)

    if np.any(y <= 0):
        raise ValueError("y must be > 0 for log(y)")

    return np.exp(x) - np.log(y)

2. Waiterクラス
今回はCopilotがeml演算子に渡す変数の組み合わせを”レシピ”と形容したことに着想を得た命名になっています。
客(呼び出し元)から注文(関数文字列)を受け取り、シェフが調理しやすいように注文用紙(木構造データ)にうつすのが仕事です。
シェフが料理したもの(計算結果)を客に提供するのも仕事です。

eml_kitchen.py
import ast
import json

class Waiter:
    """
    arguments: order: 注文された関数文字列
               value: 注文された関数に渡す入力値
    abstract: 注文された関数文字列を解析して木構造を作り、適切なデータ構造に変換する。データ構造をもとにchefに注文を渡し、渡ってきた処理結果を呼び出し元に返す。
    """

    def __init__(self, order: str, value):
        self.order = order
        self.value = value
    
    def take_order(self):
        # 注文を受け取る
        self.prepare_order()
        self.send_to_chef()
        return self.serve_customer()
    
    def prepare_order(self):
        # 注文された関数文字列を解析して木構造を作る
        self.tree = ast.parse(self.order, mode = "eval")

        # 木構造をもとに適切なデータ構造(order_ticket)に変換するための前処理を行う
        self.order_ticket = self._parse_node(self.tree.body)

    def send_to_chef(self):
        # 木構造をもとに適切なデータ構造(order_ticket)に変換し、chefに注文を渡す
        chef_instance = Chef(self.order_ticket, self.value)
        self.dish = chef_instance.cook()
    
    def _parse_node(self, node):
        # 数値
        if isinstance(node, ast.Constant):
            return node.value

        # 変数
        if isinstance(node, ast.Name):
            return node.id

        # 関数呼び出し
        if isinstance(node, ast.Call):
            func_name = node.func.id

            return {
                "type": func_name,
                "args": [self._parse_node(arg) for arg in node.args]
            }

        # 二項演算
        if isinstance(node, ast.BinOp):
            op_map = {
                ast.Add: "add",
                ast.Sub: "sub",
                ast.Mult: "mul",
                ast.Div: "div",
                ast.Pow: "pow"
            }

            return {
                "type": op_map[type(node.op)],
                "args": [
                    self._parse_node(node.left),
                    self._parse_node(node.right)
                ]
            }

        raise ValueError(f"Unsupported syntax: {node}")

    def serve_customer(self):
        # chefから渡ってきた処理結果を呼び出し元に返す
        return self.dish

3.Chefクラス
注文用紙(データ構造)を受け取り、レシピ(emlに渡す組み合わせ)を探してemlを計算し、Waiterに計算結果を渡すのが仕事です。

eml_kitchen.py
from execute_eml import eml

class Chef:
    """
    arguments: order_ticket: 注文票(waiterから渡されるデータ構造)
                value: 注文された関数に渡す入力値(waiterから渡される)
    abstract: 注文票をもとに注文された関数を実行し、処理結果をwaiterに返す。
    """

    def __init__(self, order_ticket, value):
        self.order_ticket = order_ticket
        self.value = value
    
    def evaluate(self, node):
        if isinstance(node, str):
            return self.value, 1

        if isinstance(node, (int, float)):
            return node, 1
        
        function_name = node["type"]

        recipe = self._get_recipe(function_name)
        
        args = [self.evaluate(arg) for arg in node["args"]]
        
        # emlに渡すx,yのペア
        return recipe(*args)
    
    def _get_recipe(self, function_name):
        with open("recipe_book.json", "r") as f:
            recipe_book = json.load(f)
        recipe = recipe_book.get(function_name)
        if recipe is None:
            raise ValueError(f"Recipe for {function_name} not found")
        return eval(recipe)

    def cook(self):
        # 注文票をもとに注文された関数を実行し、処理結果をwaiterに返す
        x, y = self.evaluate(self.order_ticket)
        return eml(x, y)

レシピ

今回はレシピはJSONにしてみました。
一旦expだけ実装です。

recipe_book.json
{
  "exp": "lambda a: (a[0], 1)"
}

実際に注文してみた

実際にウェイターを呼び出してみましょうか。

main.py
from eml_kitchen import Waiter

waiter = Waiter("exp(x)", 2)
print(waiter.take_order())
 $ python main.py 
7.38905609893065

これができてなんなの?

それはそうです。私もライブラリで一行で済む処理に何時間かけるのか、何度も虚しくなりました。
しかしここからが本領発揮です。
仮にこのクラスたちが、入力のexpを実装するために実装したものだったとします。データ活用が求められる時代なので、計算需要というのもきっとあるでしょう。
しかしここでユーザの気が変わります。

「exp(x**2+x-1)も計算できるようにしてほしい」

仮に直接expを実行する処理だった場合、追加でx**2+x-1を計算する処理を開発する必要があります。そしてこのシステムが本番稼働中なら、以下の過程を踏む必要があります。

1.アップデートリリースの計画を立て
2.ユーザに事前通知して稼働を停止し
3.アプリをデプロイして初稼働確認をする

では、emlだったらどうでしょうか。

eml計算のアップデート

emlを用いた計算機能の場合、実行コードは変える必要がありません。レシピだけ変えればいいのです。

recipe_book.json
{
  "exp": "lambda a: (a[0], 1)",
  "pow": "lambda a, b: (a[0] ** b[0], 1)",
  "add": "lambda a, b: (a[0] + b[0], 1)",
  "sub": "lambda a, b: (a[0] - b[0], 1)"
}

これだけ。必要なレシピを追加しただけです。
あとはウェイターにexp(x**2+x-1)と注文すれば料理が出てきます。

メリット

改めてemlを用いて計算を実装するメリットについて所見です。
emlを使わなくても、大抵の処理はライブラリから必要な機能を呼び出せば容易に実装できます。
しかしこの場合、機能追加をする際にコードを都度修正する必要があり、実際にリリースしているとなると、大したことないリリースのため手間暇かける必要がでてしまいます。

この問題を解決する方法はemlだけではありません。
計算機能を機能単位でマイクロサービス化すればいいのです。そうすれば機能追加は容易になり、リリースの際も既存サービスを止める必要はない。
しかしこれは、大したことない機能をこまごまと管理する必要があり、ハード面でのコスパが悪いです。

一方でemlを用いた場合です。機能追加でコードを修正する必要は一切ありません。
今回はJSONにしましたが、例えば計算アプリとは別のDBなどにレシピを収めれば、アプリを止める必要もないのです。
負荷が集中するという課題も、eml単体を強化すればいいので、コストを抑えられると思われます。

結論

今回はeml演算子を用いて、アプリを触らずに計算処理を追加することができました。
まぁそもそも前提として計算処理が求められるかというのがありますが、データ活用が求められる時代なので、きっとあるでしょう。
割と数学をやってる人々は論文を見ても「これがわかってもなぁ」という感想が多そうでしたが、正直システム管理の側面から見れば、かなり強い影響があると、個人的には思いました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?