こんにちは、ロジカル・アーツの井川です。
以前 Step Functions についての記事を書きましたが、そこでは扱わなかった並列処理を扱ってみたいと思います。
なお、記事の最後の方に今回作成するリソースを CloudFormation テンプレート化したものを載せてあります。必要であればご活用ください。
はじめに
今回はテーマとして、入力として 1 以上の数(10 進数)が与えられたとき、その数がある数学的な数(特殊数)かどうかを判定したいと思います。
ここでは次の 3 つの数を対象とします。これらの判定を、Step Functions の並列処理で行います。
ハッピー数
各桁の数の平方数の総和をとることを繰り返したとき、1 になる数のこと。
例
\begin{align} 79 \rightarrow 7^{2} + 9^{2} = 130 \\ 130 \rightarrow 1^{2} + 3^{2} + 0^{2} = 10 \\ 10 \rightarrow 1^{2} + 0^{2} = 1 \end{align}
ハーシャッド数
各桁の数の総和が自身を割り切る数のこと。
例
\begin{align}1 + 1 + 1 = 3|111\end{align}
※ は が を割り切ることを表す。
ナルシシスト数
桁の数で、各桁の数の 乗の総和が自身に一致する数のこと。
例
\begin{align}3^{3} + 7^{3} + 0^{3} = 370\end{align}
余談ですが、これらの数の選定基準はおおよそ次の通りです:
- 名前が面白い(気がする)
- 十分大きな数まで存在する*1
- 判定がそれほど複雑でない
Step Functions の使い方
ある程度は前回扱ったのもあって、今回は Parallel
と ResultSelector
のみ、記事中での用途に関して説明を加えたいと思います。その他については、以前の記事の該当箇所を参照ください。
Step Functions でループ処理をしてみた - Blogical
ステート(状態)
ステートは受け取った入力に対して特定の処理を行い、その結果を別のステートに渡すことができます。全てのステートは Type
(ステートの実行の種類を表す)フィールドを持っています。
Type: Parallel
並列ブランチを作成することができます。 特徴として、以下があります。
Parallel
ステートでは、各ブランチが実行され、全てのブランチが終了状態に達するまでは次のステートに移行しない- 各ブランチの処理は、ブランチ内で完結する必要がある(そのブランチで定義されていないステートに移行することはできない)
- 各ブランチの入力は、
Parallel
ステートの入力データのコピーが渡され、出力は各ブランチの出力を要素に持つ配列となる
Parallel
ステートの持つフィールドには次のものがあります。
- Branches(必須)
- 並列に実行する内容を定義した配列。
この配列の各要素は、ちょうど大元のステートマシンが持つようにStates
とStartAt
フィールドを持つ必要があります。 - ResultSelector(オプション)
- 下記の「入出力処理」の項を参照。
入出力処理
ステートの入出力は、Parameters
フィールドや ResultPath
フィールドなどを使用して、フィルタリング及び制御できます。以下の図は公式ドキュメントからの引用ですが、どういった流れでフィルタ及び制御がされるかを分かり易く表しています。
上で述べた通り、ResultSelector
のみ説明します。
- ResultSelector
- キーと値のペアを
ResultPath
に渡します。値は静的、もしくはステートの結果から選択できます。ステートの結果を使用する場合、その JSON キーの末尾に.$
が必要です。値は$.field_name
で参照します。静的な値を追加する場合は、単にキーと値のペアを追記します。
Lambda 関数を使った並列処理
「はじめに」でも述べた通り、入力として与えられた 1 以上の数が特殊数であるかを判定します。
以下に各ステートの概要と定義コードを載せてあります。出来上がるワークフローは次の図のようになります。点線で囲まれた枠にある文字列が各ステート(の名前)を表し、矢印がステートの移行先を表しています。
実装ステート一覧
- IsPositive
-
- 概要:
-
一番最初に実行されるステートです。ステートマシンに対する入力が、このステートの入力になります。入力は
{"number": n}
の形とします。number
の値 n が正の整数でないならInvalid
ステートに移行し、 そうでなければIsSpecialNumbers
ステートに移行します。 - Type:
Choice
- Next:
Invalid
またはIsSpecialNumbers
- Amazon ステート言語:
-
"IsPositive": { "Type": "Choice", "Choices": [ { "Variable": "$.number", "NumericLessThanEquals": 0, "Next": "Invalid" } ], "Default": "IsSpecialNumbers" }
- IsSpecialNumbers
-
- 概要:
-
次の形の入力を持ちます:
{"number": n}
Branches
フィールドで定義されている各タスクを実行します。各タスクはnumber
の値 n がその特殊数であるかに応じてTrue
またはFalse
を返します。
ResultSelector
を用いてこのステートの出力を{"IsHappy": IsHappy の実行結果, "IsHarshad": IsHarshad の実行結果, "IsNarcissistic": IsNarcissistic の実行結果}
のようにします。
Result
ステートに移行します。 - Type:
Parallel
- Branches:
-
- IsNarcissistic
- 概要:
-
次の形の入力を持ちます:
{"number": n}
Lambda 関数IsNarcissistic
を呼び出します。この Lambda 関数はnumber
の値 n がナルシシスト数であればTrue
を、そうでなければFalse
を返します。 - Type:
Task
- Amazon ステート言語:
-
{ "StartAt": "IsNarcissistic", "States": { "IsNarcissistic": { "Type": "Task", "Resource": "${IsNarcissistic の ARN}", "End": true } } }
- Next:
Result
- Amazon ステート言語:
-
"IsSpecialNumbers": { "Type": "Parallel", "Branches": [ { "StartAt": "IsHappy", "States": { "IsHappy": { "Type": "Task", "Resource": "${IsHappy の ARN}", "End": true } } }, { "StartAt": "IsHarshad", "States": { "IsHarshad": { "Type": "Task", "Resource": "${IsHarshad の ARN}", "End": true } } }, { "StartAt": "IsNarcissistic", "States": { "IsNarcissistic": { "Type": "Task", "Resource": "${IsNarcissistic の ARN}", "End": true } } } ], "ResultSelector": { "IsHappy.$": "$[0]", "IsHarshad.$": "$[1]", "IsNarcissistic.$": "$[2]" }, "Next": "Result" }
- Result
-
- 概要:
-
ステートマシンの実行を正常に終了します。
- Type:
Succeed
- Amazon ステート言語:
-
"Result": { "Type": "Succeed" }
- Invalid
-
- 概要:
-
ステートマシンの実行を停止し、失敗として記録します。
- Type:
Fail
- Cause:初期値が正の整数ではありませんでした。
- Error:NotPositiveNumberError
- Amazon ステート言語:
-
"Invalid": { "Type": "Fail", "Cause": "初期値が正の整数ではありませんでした。", "Error": "NotPositiveNumberError" }
Lambda 関数の作成
Step Functions から Lambda 関数を呼び出すときに、その ARN が必要になりますので、あらかじめ Lambda 関数を作成します。
作成手順は次の通りです。
Lambda のコンソール画面に移動し、「関数の作成」をクリックします。
次の画面に移行したら、「一から作成」を選択し、「関数名」は作成する関数に応じて「IsHappy」、「IsHarshad」または「IsNarcissistic」を入力します。(以下の画像は「IsHappy」関数作成時のものです。)ランタイムは「Python 3.9」を選択し、「関数の作成」をクリックします。
画面が遷移したら、lambda_function.py
のコードを作成した関数に応じた、以下のコードに全て置き換えます。
IsHappy
def lambda_handler(event, context): return is_happy(event["number"]) def is_happy(n): """与えられた数がハッピー数がどうか判定する。 Args: n (int): 自然数(10進数) Returns: bool: ハッピー数であれば `True` """ sn = str(n) appeared_nums = set() while n > 1 and (n not in appeared_nums): appeared_nums.add(n) n = sum(map(lambda x: int(x) ** 2, sn)) sn = str(n) return n == 1
IsHarshad
def lambda_handler(event, context): return is_harshad(event["number"]) def is_harshad(n): """与えられた数がハーシャッド数がどうか判定する。 Args: n (int): 自然数(10進数) Returns: bool: ハーシャッド数であれば `True` """ factor = sum([int(x) for x in str(n)]) return n % factor == 0
IsNarcissistic
def lambda_handler(event, context): return is_narcissistic(event["number"]) def is_narcissistic(n): """与えられた数がナルシシスト数がどうか判定する。 Args: n (int): 自然数(10進数) Returns: bool: ナルシシスト数であれば `True` """ sn = str(n) digit_num = len(sn) return sum(map(lambda x: int(x) ** digit_num, sn)) == n
ステートマシンの作成
Step Functions のコンソール画面に行き、「ステートマシンの作成」をクリックします。
「コードでワークフローを記述」を選択し、タイプは「標準」を選択します。
そのまま下にスクロールし、左側の赤枠で囲ったところに以下のコードを貼り付けます。ARN のところは作成した Lambda 関数のものに置き換えて下さい。定義に不備がなければ、右側の状態遷移図が画像のようになります。確認できたら「次へ」をクリックします。
{ "Comment": "与えられた数が、特殊数かどうかを判定するステートマシン。", "StartAt": "IsPositive", "States": { "IsPositive": { "Type": "Choice", "Choices": [ { "Variable": "$.number", "NumericLessThanEquals": 0, "Next": "Invalid" } ], "Default": "IsSpecialNumbers" }, "IsSpecialNumbers": { "Type": "Parallel", "Branches": [ { "StartAt": "IsHappy", "States": { "IsHappy": { "Type": "Task", "Resource": "${IsHappy の ARN}", "End": true } } }, { "StartAt": "IsHarshad", "States": { "IsHarshad": { "Type": "Task", "Resource": "${IsHarshad の ARN}", "End": true } } }, { "StartAt": "IsNarcissistic", "States": { "IsNarcissistic": { "Type": "Task", "Resource": "${IsNarcissistic の ARN}", "End": true } } } ], "ResultSelector": { "IsHappy.$": "$[0]", "IsHarshad.$": "$[1]", "IsNarcissistic.$": "$[2]" }, "Next": "Result" }, "Result": { "Type": "Succeed" }, "Invalid": { "Type": "Fail", "Cause": "初期値が正の整数ではありませんでした。", "Error": "NotPositiveNumberError" } } }
「名前」に 「SpecialNumberChecker」 と入力します。他のところはそのままにして、 画面下部の「ステートマシンの作成」 をクリックします。
作成されたらさっそく実行してみましょう。「実行の開始」をクリックすると、二枚目の画像の画面が出てきます。入力を {"number": 7}
のような形にして、「実行の開始」をクリックすればステートマシンが実行されます。
しばらく待つと結果が確認できます。グラフ(ビジュアルワークフロー)の Result
ステートの箇所をクリックすると、特殊数の判定結果が確認できます。全て true
になっているので、 はハッピー数であり、ハーシャッド数であり、なおかつナルシシスト数であることが分かります*2。
おわりに
並列処理の用途としては、複数の処理を同時に実行し、それらすべてが完了してから次の処理を行う必要がある場合になると思います。今回は例として単に数値の判定を行うだけのもでしたが、より複雑なユースケースにも対応できると思います。
Appendix: CloudFormation テンプレート
今回作成したリソースを CloudFormation テンプレート化したものを載せておきます。リソースを作成するのに十分な権限さえあれば、こちらのテンプレートを使用して記事中のステートマシンを作成できると思います。
AWSTemplateFormatVersion: 2010-09-09 Description: >- Step Functions 並列処理のブログ記事用テンプレート。 Resources: IsHappyFn: Type: AWS::Lambda::Function Properties: Code: ZipFile: | def lambda_handler(event, context): return is_happy(event["number"]) def is_happy(n): """与えられた数がハッピー数がどうか判定する。 Args: n (int): 自然数(10進数) Returns: bool: ハッピー数であれば `True` """ sn = str(n) appeared_nums = set() while n > 1 and (n not in appeared_nums): appeared_nums.add(n) n = sum(map(lambda x: int(x) ** 2, sn)) sn = str(n) return n == 1 Description: 与えられた数がハッピー数がどうか判定する関数。 FunctionName: IsHappy Handler: index.lambda_handler Role: !GetAtt LambdaExecutionRole.Arn Runtime: python3.9 IsHarshadFn: Type: AWS::Lambda::Function Properties: Code: ZipFile: | def lambda_handler(event, context): return is_harshad(event["number"]) def is_harshad(n): """与えられた数がハーシャッド数がどうか判定する。 Args: n (int): 自然数(10進数) Returns: bool: ハーシャッド数であれば `True` """ factor = sum([int(x) for x in str(n)]) return n % factor == 0 Description: 与えられた数がハーシャッド数がどうか判定する関数。 FunctionName: IsHarshad Handler: index.lambda_handler Role: !GetAtt LambdaExecutionRole.Arn Runtime: python3.9 IsNarcissisticFn: Type: AWS::Lambda::Function Properties: Code: ZipFile: | def lambda_handler(event, context): return is_narcissistic(event["number"]) def is_narcissistic(n): """与えられた数がナルシシスト数がどうか判定する。 Args: n (int): 自然数(10進数) Returns: bool: ナルシシスト数であれば `True` """ sn = str(n) digit_num = len(sn) return sum(map(lambda x: int(x) ** digit_num, sn)) == n Description: 与えられた数がナルシシスト数がどうか判定する関数。 FunctionName: IsNarcissistic Handler: index.lambda_handler Role: !GetAtt LambdaExecutionRole.Arn Runtime: python3.9 ParallelSFN: Type: AWS::StepFunctions::StateMachine Properties: DefinitionString: |- { "Comment": "与えられた数が、特殊数かどうかを判定するステートマシン。", "StartAt": "IsPositive", "States": { "IsPositive": { "Type": "Choice", "Choices": [ { "Variable": "$.number", "NumericLessThanEquals": 0, "Next": "Invalid" } ], "Default": "IsSpecialNumbers" }, "IsSpecialNumbers": { "Type": "Parallel", "Branches": [ { "StartAt": "IsHappy", "States": { "IsHappy": { "Type": "Task", "Resource": "${IsHappyFnArn}", "End": true } } }, { "StartAt": "IsHarshad", "States": { "IsHarshad": { "Type": "Task", "Resource": "${IsHarshadFnArn}", "End": true } } }, { "StartAt": "IsNarcissistic", "States": { "IsNarcissistic": { "Type": "Task", "Resource": "${IsNarcissisticFnArn}", "End": true } } } ], "ResultSelector": { "IsHappy.$": "$[0]", "IsHarshad.$": "$[1]", "IsNarcissistic.$": "$[2]" }, "Next": "Result" }, "Result": { "Type": "Succeed" }, "Invalid": { "Type": "Fail", "Cause": "初期値が正の整数ではありませんでした。", "Error": "NotPositiveNumberError" } } } DefinitionSubstitutions: IsHappyFnArn : !GetAtt IsHappyFn.Arn IsHarshadFnArn : !GetAtt IsHarshadFn.Arn IsNarcissisticFnArn : !GetAtt IsNarcissisticFn.Arn RoleArn: !GetAtt ParallelSFNExecRole.Arn StateMachineName: SpecialNumberChecker LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: LambdaExecutionRole PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:* Resource: arn:aws:logs:*:*:* ParallelSFNExecRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: states.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: ParallelSFNExecRole PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - lambda:InvokeFunction Resource: - !GetAtt IsHappyFn.Arn - !GetAtt IsHarshadFn.Arn - !GetAtt IsNarcissisticFn.Arn