概要
CELは標準的な演算子や関数に加え、独自のカスタム関数を定義して機能を拡張することが可能です。
今回はカスタム関数を使ってみる際に必要な前提知識を踏まえながらサンプルコードを紹介します。
環境
- go v1.22.1
- cel-go v0.20.1
カスタム関数の作り方
Goでの実装になりますが、手順としては以下です。
- environmentに関数を登録
- bindする関数を定義(interfaceを実装する)
- 評価式で関数を使う
事前知識
事前に知っておくと理解が早くなるのであらかじめ説明します。
OverloadとMemberOverloadの違い
Overload
次のように通常の関数のように定義する際はOverload
を使います。
concatStr(arg1, arg2)
MemberOverload
次のようにメンバ(target)に関数を生やす際はMemberOverload
を使います。
target.join(arg1)
overload IDの命名規則
environmentで関数を登録する際に、先程のoverloadにおいてid名を登録する必要があるのですが、
func Overload(overloadID string, args []*Type, resultType *Type, opts ...OverloadOpt) FunctionOpt func MemberOverload(overloadID string, args []*Type, resultType *Type, opts ...OverloadOpt) FunctionOpt
その命名規則は以下となっています。
Overload
Overloadはfunc_argType_argType
という命名規則です。
例としてconcatStr("alice", 10)
のような関数を定義する場合は
concatStr_string_int
と命名します。
MemberOverload
MemberOverloadはtargetType_func_argType_argType
という命名規則です。
例としてi.greet(you)
のような関数を定義する場合(i
, you
が文字列とします)、
string_greet_string
と命名します。
UnaryBinding, BinaryBinding, FunctionBindingの違い
先ほど
- bindする関数を定義(interfaceを実装する)
と説明した際に、bindする関数のinterfaceが引数の数によって異なるのでその違いを説明します。
UnaryBinding
引数1つ取ります。戻り値は1つです。
func UnaryBinding(binding functions.UnaryOp) OverloadOpt
type UnaryOp func(value ref.Val) ref.Val
ref: https://pkg.go.dev/github.com/google/cel-go/cel#UnaryBinding
BinaryBinding
引数2つとります。戻り値は1つです。
func BinaryBinding(binding functions.BinaryOp) OverloadOpt
type BinaryOp func(lhs ref.Val, rhs ref.Val) ref.Val
ref: https://pkg.go.dev/github.com/google/cel-go/cel#BinaryBinding
FunctionBinding
引数は可変長です。戻り値は1つです。
func FunctionBinding(binding functions.FunctionOp) OverloadOpt
type FunctionOp func(values ...ref.Val) ref.Val
ref: https://pkg.go.dev/github.com/google/cel-go/cel#FunctionBinding
まとめるとこうなります。
binding | 引数 | 戻り値 |
---|---|---|
UnaryBinding | 1 | 1 |
BinaryBinding | 2 | 1 |
FunctionBinding | 可変長 | 1 |
戻り値はどれも1つですが、ListTypeやMapTypeを返すことで擬似的に複数返すことも可能です。
具体的な実装
それでは具体的な実装例を紹介します。
今回は2つの文字列を結合するconcatStr
という関数を独自定義します。
グローバルな関数を作るケース
func global() { env, err := cel.NewEnv( cel.Function("concatStr", // グローバルな関数なのでOverloadを使う // 命名規則はfunc_argType_argType cel.Overload("concatStr_string_string", // 引数の型を指定 []*cel.Type{cel.StringType, cel.StringType}, // 戻り値の型を指定 cel.StringType, // 引数が2つの関数を用意するのでBinaryBinding cel.BinaryBinding(concatFunc), ), ), ) if err != nil { log.Fatalf("failed to create env: %s\n", err) } // 評価式でカスタム関数を呼び出す。今回は変数なし expr := `concatStr('Hello', 'World')` ast, iss := env.Compile(expr) if iss != nil && iss.Err() != nil { log.Fatalf("failed to compile expression: %v", iss.Err()) } p, err := env.Program(ast) if err != nil { log.Fatalf("failed to create program: %s\n", err) } // 変数なしなので空のmapを渡す out, _, err := p.Eval(map[string]interface{}{}) if err != nil { log.Fatalf("evaluation error: %s\n", err) } fmt.Println("Result:", out) }
BindするconcatFuncはBinaryOpを実装する形で定義します。
func concatFunc(arg1, arg2 ref.Val) ref.Val { v1 := arg1.(types.String) v2 := arg2.(types.String) return types.String(v1 + v2) }
評価式にベタ書きなので、それが結合されて返ってきます。
Result: HelloWorld
グローバルな関数に引数をもたせる
func globalWithVariableArg(a, b string) { env, err := cel.NewEnv( // 引数を変数として定義 cel.Variable("arg1", cel.StringType), cel.Variable("arg2", cel.StringType), cel.Function("concatStr", // グローバルな関数なのでOverloadを使う cel.Overload("concatStr_string_string", // 引数の型を指定 []*cel.Type{cel.StringType, cel.StringType}, // 戻り値の型を指定 cel.StringType, // 引数が2つの関数を用意するのでBinaryBinding cel.BinaryBinding(concatFunc), ), ), ) if err != nil { log.Fatalf("failed to create env: %s\n", err) } // 評価式でカスタム関数を呼び出す。今回は変数あり expr := `concatStr(arg1, arg2)` ast, iss := env.Compile(expr) if iss != nil && iss.Err() != nil { log.Fatalf("failed to compile expression: %v", iss.Err()) } p, err := env.Program(ast) if err != nil { log.Fatalf("failed to create program: %s\n", err) } // 入力値として変数を渡す out, _, err := p.Eval(map[string]interface{}{ "arg1": a, "arg2": b, }) if err != nil { log.Fatalf("evaluation error: %s\n", err) } fmt.Println("Result:", out) }
引数を渡して呼び出すと
globalWithVariableArg("I'm ", "Alice")
次のように結合されます。
Result: I'm Alice
メンバーに関数を生やす
レシーバーにメソッドをつけるような形式でカスタム関数を定義することもできます。
func memberWithVariableArg(a, b string) { env, err := cel.NewEnv( // メンバ、引数を変数として定義 cel.Variable("target", cel.StringType), cel.Variable("arg1", cel.StringType), cel.Function("concatStr", // メンバ関数の場合はMemberOverloadを使う // 命名規則はtargetType_func_argType_argType cel.MemberOverload("string_concatStr_string", // 引数の型を指定。第一引数がメンバの型になる []*cel.Type{cel.StringType, cel.StringType}, // 戻り値の型を指定 cel.StringType, // 引数が2つの関数を用意するのでBinaryBinding cel.BinaryBinding(concatFunc), ), ), ) if err != nil { log.Fatalf("failed to create env: %s\n", err) } // メンバ関数の場合は、メンバに関数を生やして記述する expr := `target.concatStr(arg1)` ast, iss := env.Compile(expr) if iss != nil && iss.Err() != nil { log.Fatalf("failed to compile expression: %v", iss.Err()) } p, err := env.Program(ast) if err != nil { log.Fatalf("failed to create program: %s\n", err) } out, _, err := p.Eval(map[string]interface{}{ "target": a, "arg1": b, }) if err != nil { log.Fatalf("evaluation error: %s\n", err) } fmt.Println("Result:", out) }
引数を渡して呼び出すと
memberWithVariableArg("I'm ", "Bob")
次のように結合されます。
Result: I'm Bob
その他
サンプルコード
今回のサンプルコードはこちら
関数内でのエラーハンドリング
Bindする関数内でのエラーハンドリングについては、関数が
の実装をしますがどれも戻り値はref.Val
のみです。
なので
types.NewErr
types.ValOrErr
types.MaybeNoSuchOverload
のいずれかを使ってエラーを返します。
以下例です。
func stringOrError(str string, err error) ref.Val { if err != nil { return types.NewErr(err.Error()) } return types.String(str) }
cel.TypeParamTypeはどんな時に使う?
codelabではcel.TypeParamType
を使ったサンプルが書かれています。
// Useful components of the type-signature for 'contains'. typeParamA := cel.TypeParamType("A") typeParamB := cel.TypeParamType("B") mapAB := cel.MapType(typeParamA, typeParamB) // Env declaration. env, _ := cel.NewEnv( cel.Types(&rpcpb.AttributeContext_Request{}), // Declare the request. cel.Variable("request", cel.ObjectType("google.rpc.context.AttributeContext.Request"), ), // Declare the custom contains function and its implementation. cel.Function("contains", cel.MemberOverload( "map_contains_key_value", []*cel.Type{mapAB, typeParamA, typeParamB}, cel.BoolType, cel.FunctionBinding(mapContainsKeyValue)), ), )
ref: https://codelabs.developers.google.com/codelabs/cel-go#7
これはジェネリクス的な使い方で、MapTypeやListTypeをジェネリクス的に汎化したい時に使えます。
もしMapの中身(key-value)がstringに限定されるのであればcel.TypeParamType
は使わずcel.StringType
で問題なく動きます。
まとめ
CELでカスタム関数を使う際に自分が気になった部分中心にサンプルを交えて説明しました。