cyamli: YAMLからCLIを自動生成するツール

概要

本記事は,OSS Advent Calendar 2023の12月14日の記事です.

qiita.com

本記事では,自作OSSとして,YAMLからCLIを自動生成するツール「cyamli」を試作しましたので,これについて紹介します.

背景

最近,Goでプログラムを書いています.その中で,作業の自動化を目的としてコンソールアプリケーションを作成することがよくあります. コンソールアプリケーションは,CLI(Command Line Interface)を備えたプログラムで,サブコマンド,オプション引数,位置引数を含むコマンドライン引数を取り扱うことが必要となります.

コマンドライン引数を取り扱うためには,コマンドライン引数を定義・解釈する仕組みとしてが必要となりますが,Goの既存のライブラリの中で以下の条件を満たすAPI(本記事では,APIApplication Programming Interface)といえばWeb APIではなく,ライブラリ等のプログラミング言語のためのインターフェースのことを指します.)を見つけることができませんでした.

  1. コマンドライン引数として,サブコマンド,オプション引数,位置引数を定義・解釈することができる.
  2. 型付けされたAPIとして提供される.
  3. SSOT(Single Source of Truth)となる.

例えば,Goの標準ライブラリにあるos.Argsは,最も原始的なAPIで,コマンドライン引数をstringを要素とするスライスとして表現しますが,1も2も満たしません. pkg.go.dev

Goの標準ライブラリにあるflagパッケージは,型付けされたオプション引数を扱うことができますが,サブコマンドや位置引数を定義する機能は持ちません.また,オプション引数の定義は,var helpFlag = flag.Bool("help", true, "show help")のように行うのですが,変数名helpFlagとオプション引数名"help"のように情報が重複してしまうことがあり,3を完全に満たしているとは言えません. pkg.go.dev

外部ライブラリであるcobraは,サブコマンド,オプション引数,位置引数を定義・解釈することができますが,位置引数の型を指定することができないため,2を完全に満たしているとは言えません.また,flagパッケージと同様にソースコード内で情報が重複してしまうことがあり,3を完全に満たしているとは言えません. github.com

別の外部ライブラリであるdocoptは,独自のフォーマットに沿って記述されるヘルプテキストによって,サブコマンド,オプション引数,位置引数を含む複雑なコマンドライン引数を定義することができ,これを元にコマンドライン引数を解釈することができますが,型を指定することができません. github.com

YAMLからCLIを自動生成するツール「cyamli」

上で示した条件1は,自分がコマンドライン引数を扱うコンソールアプリケーションを開発する上で,必要だと感じた最低限の機能です. 条件2は,エディタや統合開発環境の支援を享受したり,コンソールアプリケーションのビルド時に網羅的な型チェックを実施するために必要ものです. 条件3は,プログラムを管理する上で,一貫性を保ちやすくするために必要なものです.

コマンドライン引数を定義・解釈する仕組みとして,これらの条件をみたすものを実現するために「cyamli」というツールを開発しました. GitHub上で公開しています. github.com

「cyamli」の仕組みと特徴

「cyamli」は,以下の図に示すような仕組みで動作します.

cyamliは,CLIの定義を記述したYAMLファイルを読み込んで,CLIを解釈するAPIを書き出す.内部では,型の構築とコード生成が行われる.
「cyamli」の仕組み

「cyamli」は以下の特徴を持ちます.

  • サブコマンド,オプション引数,位置引数をサポートします.
  • CLI定義の情報源が1つのYAMLに集約されるため,SSOTが実現できます.
  • コンソールアプリケーションをビルドする前にCLIの型が決定されるため,型付けされたAPIを提供することができます.

以下は,前節で挙げたライブラリと「cyamli」を比較する表です.

「cyamli」は「機能」「型付け」「SSOT」を全て満たす.
前節のライブラリと「cyamli」の比較

「cyamli」の使い方と使用例

「cyamli」の使い方は,次の通りです.

  1. CLIYAMLで定義する.
  2. CLIを解釈するAPIを自動生成する.
  3. 自動生成されたAPIに関数を割り当てる.

使用例を以下に示します.

1. CLIYAMLで定義

ここでは例として,データベースからテーブルの情報を取得するコンソールアプリケーションの作成を想定して,以下のようにCLIを定義します.

# cli.yaml
name: demo
description: demo app to get table information from databases
subcommands:
  list:
    description: list tables
    options: 
      -config:
        description: path to config file
        short: -c
  describe:
    description: show information of tables
    options: 
      -config:
        description: path to config file
        short: -c
      -format:
        short: -f
        description: "output format: json or sql"
        default: json
      -unique:
        description: includes unique constraint information
        type: boolean
      -foreign-key:
        description: includes foreign key constraint information
        type: boolean
      -check:
        description: includes check constraint information
        type: boolean
    arguments:
      - name: tables
        variadic: true
        description: names of tables to be described

このYAMLファイルでは,コンソールアプリケーションについて以下の内容が定義されています.

  • 名前がdemoである.
  • サブコマンドとしてlistdescribeを持つ.

また,listサブコマンドについては以下の内容が定義されています.

  • 以下のオプション引数を持つ.
    • -config:文字列型の値を取り,コンフィグファイルのパスを指定する.短縮バージョンとして-cがある.

describeサブコマンドについては以下の内容が定義されています.

  • 以下のオプション引数を持つ.
    • -config:コンフィグファイルのパスを指定する.短縮バージョンとして-cがある.
    • -format:出力のフォーマットを指定する.短縮バージョンとして-fがある.デフォルト値は"json"である.
    • -uniqueブーリアン型の値を取り,ユニーク制約を含めるかどうかを指定する.
    • -checkブーリアン型の値を取り,チェック制約を含めるかどうかを指定する.
    • -foreign-keyブーリアン型の値を取り,外部キー制約を含めるかどうかを指定する.
  • 以下の位置引数を持つ.
    • tables:取得対象のテーブルを0個以上指定する.

2. CLIを解釈するAPIを自動生成

「cyamli」を実行してAPIを生成します. 「cyamli」の実行は以下のように行うことができます.

# cyamli のインストール
go install "github.com/Jumpaku/cyamli/cmd/cyamli@latest"

# cyamliの実行
# cli.yamlを入力し,cli.gen.goに出力する.
cyamli golang -schema-path=cli.yaml -out-path=cli.gen.go

単に以下のように実行することもできます.

go run "github.com/Jumpaku/cyamli/cmd/cyamli@latest" golang -schema-path=cli.yaml -out-path=cli.gen.go

これにより,以下のようなAPIが生成されます(一部省略).

// Code generated by cyamli v0.0.11, DO NOT EDIT.
package main

// ...

type Func[Input any] func(subcommand []string, input Input, inputErr error) (err error)

type CLI struct {
    List     CLI_List
    Describe CLI_Describe
    FUNC     Func[CLI_Input]
}
type CLI_Input struct {
}

type CLI_List struct {
    FUNC Func[CLI_List_Input]
}
type CLI_List_Input struct {
    Opt_Config string
}

type CLI_Describe struct {
    FUNC Func[CLI_Describe_Input]
}
type CLI_Describe_Input struct {
    Opt_Check      bool
    Opt_Config     string
    Opt_ForeignKey bool
    Opt_Format     string
    Opt_Unique     bool
    Arg_Tables     []string
}

// ...

func NewCLI() CLI {
    return CLI{}
}


func Run(cli CLI, args []string) error {
    // ...
}

このように,cyamliが生成するコードには以下のAPIが含まれます.

  • CLI:ルートコマンドを表現する構造体
  • CLI_Input:ルートコマンドに渡すコマンドライン引数を表現する構造体
  • CLI_<サブコマンド>:サブコマンドを表現する構造体
  • CLI_<サブコマンド>_Input:サブコマンドに渡すコマンドライン引数を表現する構造体
  • Run:エントリーポイントとなる関数

3. 自動生成されたAPIに関数を割り当て

CLI構造体とCLI_<サブコマンド>構造体は,FUNCフィールドを持ちます.FUNCフィールドは,コマンドライン引数を表現する構造体を受け取ることが可能な関数の型を持っており,コマンドが実行するべき処理を設定することができます.

以下は,コマンドが実行するべき処理を設定したmain.goの例です.

// main.go
package main

import (
    _ "embed"
    "fmt"
    "os"
)

func main() {
    // CLIオブジェクトを生成
    cli := NewCLI()

    // ルートコマンドの処理を設定
    cli.FUNC = func(subcommand []string, input CLI_Input, inputErr error) (err error) {
        fmt.Printf("%#v\n", input)
        fmt.Println(cli.DESC_Detail())
        return inputErr
    }
    // listサブコマンドの処理を設定
    cli.List.FUNC = func(subcommand []string, input CLI_List_Input, inputErr error) (err error) {
        fmt.Printf("%#v\n", input)
        fmt.Println(cli.List.DESC_Detail())
        return inputErr
    }
    // describeサブコマンドの処理を設定
    cli.Describe.FUNC = func(subcommand []string, input CLI_Describe_Input, inputErr error) (err error) {
        fmt.Printf("%#v\n", input)
        fmt.Println(cli.Describe.DESC_Detail())
        return inputErr
    }

    // コマンドライン引数の解釈と対応する処理の呼び出しを実行
    if err := Run(cli, os.Args); err != nil {
        panic(err)
    }
}

以下は,ルートコマンドの実行例です.

go build -o demo . && ./demo

main.CLI_Input{}
demo:
demo app to get table information from databases

Usage:
    $ demo


Subcommands:
    describe:
        show information of tables

    list:
        list tables

自動生成されたヘルプテキストが利用できることがわかります.

以下は,listサブコマンドの実行例です.

go build -o demo . && ./demo list -c=./demo.config

main.CLI_List_Input{Opt_Config:"./demo.config"}
list tables

Usage:
    $ <program> list [<option>]...


Options:
    -config=<string>, -c=<string>  (default=""):
        path to config file

短縮バージョンのオプション引数-cで指定した文字列"./demo.config"を,オプション引数-configの値として渡すことができたことがわかります.

以下は,describeサブコマンドの実行例です.

go build -o demo . && ./demo describe -check Table1 Table2 Table3

main.CLI_Describe_Input{Opt_Check:true, Opt_Config:"", Opt_ForeignKey:false, Opt_Format:"json", Opt_Unique:false, Arg_Tables:[]string{"Table1", "Table2", "Table3"}}
show information of tables

Usage:
    $ <program> describe [<option>|<argument>]... [-- [<argument>]...]


Options:
    -check[=<boolean>]  (default=false):
        includes check constraint information

    -config=<string>, -c=<string>  (default=""):
        path to config file

    -foreign-key[=<boolean>]  (default=false):
        includes foreign key constraint information

    -format=<string>, -f=<string>  (default="json"):
        output format: json or sql

    -unique[=<boolean>]  (default=false):
        includes unique constraint information


Arguments:
    [0:] [<tables:string>]...
        names of tables to be described

オプション引数-formatにデフォルト値である"json"が渡されていることがわかります. また,オプション引数-checkにより,ブーリアン型の値trueが渡されていることがわかります. さらに,位置引数を可変長とすることも可能であることがわかります.

まとめと展望

以下に本記事のまとめを示します.

  • コンソールアプリケーションを作成する際には,CLI(サブコマンド,オプション引数,位置引数を含むコマンドライン引数)を定義・解釈する仕組みが必要となります.
  • YAMLからCLIを自動生成するというアプローチにより,この仕組みを実現する「cyamli」を開発し,公開しました.
  • 「cyamli」を紹介し,型付けされたAPIを提供できること,SSOTを実現できることを示しました.

以下に展望を示します.

  • 「cyamli」は,その内部のコード生成処理を置き換えることで,様々なプログラミング言語をサポートすることが可能なため,今後はサポートするプログラミング言をGo以外にも広げていきたいと考えています.
  • 本記事を読んでくださった方には,ぜひ「cyamli」を使ってみてもらいたいと思っています.

追いコンを開催した

はじめに

本記事は,競技プログラミング Advent Calendar 2022の12月23日の記事です. qiita.com


追いコンとは,追い出しコンテストの省略形で,組織から抜ける者が組織に残る者に対して開催するコンテストを指します. 例えば,大学のサークルなどでは,卒業生がお世話になった下級生に対して感謝の思いを込めて競技プログラミングコンテストを開催することがよくあります. 実際私も,先輩が自身の卒業のタイミングで開催した競技プログラミングコンテストに参加させていただいたことがあります. chut1130m.hatenablog.com

そして,2022年の9月30日に,私も無事に半年留年して博士課程を修了したため,それまでお世話になったプログラミングサークルの後輩に感謝の思いを込めて追いコンを開催しました. 本記事では,この追いコンについて述べます.

追いコンの概要

追いコンの概要は以下の通りです.

  • 参加者:私の所属するプログラミングサークルの中で,お世話になった方々(コンテスタント7人,協力者1人)
  • 時間:1時間30分
  • 問題数:30問
  • 難易度:AtCoder Beginner Contestの100点から500点までの間程度
  • 日時:2022年12月11日の18:00から19:30まで
  • 会場:Discord
  • プラットフォーム:QingdaoU/OnlineJudge github.com

準備

以下の準備を行いました.

  1. 参加者の確保と日程調整
  2. プラットフォームの準備
  3. 問題の作成

参加者の確保と日程調整

まず,開催に必要な最低参加者数を,4人と定めた上で,最も参加してくれそうな数人に声をかけ,参加を確約してもらいました. 彼らをコア参加者と呼ぶこととします. コア参加者を確保できたことにより,最低参加者数が達成できました. また,協力者も1人確保しました.

次に,コア参加者が参加可能な日程を複数確保し,これを開催候補日程としました.

その後,顔を思い出せるサークルメンバーにDiscordでDMを送って追いコンに誘い,参加したい場合は開催候補日程の中から参加可能な日程を教えてもらい日程調整を行いました. 具体的には,調整さんというサービスを利用し,開催候補日程の中で参加可能な人の最も多い日程のうち最も近い日程に決定しました. chouseisan.com

プラットフォームの準備

追いコンのプラットフォームとして,QingdaoU/OnlineJudgeというオンラインジャッジシステムを使用することとしました. このシステムはOSSのオンラインジャッジシステムで,コンテストサイトとジャッジサーバーを含むdocker-compose.ymlを起動するだけで準備が整います. https://github.com/QingdaoU/OnlineJudgeDeploy/blob/2.0/README.en.md

実際の作業として以下の作業を行いました.

  1. 私が契約しているVPS内でオンラインジャッジシステムを起動する.
  2. 私が持っているドメインにコンテストサイト用のサブドメインを設定する.
  3. VPS内のリバースプロキシにコンテストサイト用の設定を加える.

ここで1つ躓いたことがありました.ローカル環境でオンラインジャッジシステムの起動を確認しようとして失敗しました.原因は定かではありませんが,ローカル環境がApple M1チップ搭載のMacBook Airだったからかもしれません.

問題の作成

準備の中で最も時間がかかったのが問題の作成でした.問題の作成では1人の協力者と協力して,問題文の作成,テストケースの作成,解答の作成,問題のチェックを行いました. 問題の構成は以下の通りとなりました.

Number Title Score
1 Hello 20
2 Naming rules 40
3 Go 20
4 Linear Transformation 30
5 Numbers 20
6 Count vowels 20
7 max - min 20
8 Compare 20
9 Memoized Recursion 50
10 GCD Rectangle 400
11 Shepherd 300
12 Segment Eraser 500
13 String Power 20
14 Sum of numbers 20
15 Tri Tri Tri 400
16 Java 20
17 Parallelogram 40
18 Many Swaps 20
19 Calc 30
20 Transaction 40
21 Recursive Parity 50
22 Quadratic Equation 50
23 MAX LCM 300
24 ASC 20
25 Sort Unique 20
26 Compare2 50
27 Compare3 40
28 Switch Case 20
29 Sorting integers as strings 20
30 Linear Equation 20

Scoreは,難易度の高い問題ほど高くなるように設定しました. AtCoder Beginner Contestで100点以上300点未満くらいの点数になりそうな難易度の問題は,大量に用意した上でScoreが100点未満となるように調整しました. AtCoder Beginner Contestで300点以上の点数になりそうな難易度の問題は,その点数をScoreとしました. このような構成としたのは,参加者のレベルにばらつきがあったとしても,難し過ぎて手が止まってしまったり,易し過ぎて時間が余ってしまったりすることがないようにするためでした.

問題の作成で最も大変だったことは,厳密な問題文を記述することでした. また,注意が必要だったこととして,追いコンで利用したオンラインジャッジシステムは,AtCoderと異なり,テストケース毎にACとなればScoreが加算されるということが挙げられます.

問題のチェックは,問題文に不備が無いか,制約は適切か,テストケースは正しいか,難易度に対するScoreは適切か,といった点について協力者と互いにチェックすることができました.

開催

追いコン当日のスケジュールは以下のようになりました.

17:30に集合して最初に,オンラインジャッジシステムのアカウントを作成してもらいました. アカウント作成の間に,私から卒業の挨拶として,同級生がおらず寂しかった博士課程の私に,構ってもらったことへの感謝を述べました. また,アルバム鑑賞ということで,私のツイートを画面共有して遡りながら思い出を懐かしみました.

18:00になるとコンテストがスタートしました. 私は協力者と一緒に順位表を見てお喋りをしたり,質問への対応をしていました.

19:00を過ぎた頃,コンテスタントからの質問により異変に気がつきました. コンテスト終了時刻を19:30と設定していたのに,コンテストが終了してしまったのです. 原因はVPSの時刻が10分以上進んでいたことだったため,ずれた時間の分だけコンテスト終了時刻を遅く設定し直しました.

19:30にコンテストが終了した後は,AmongUsをして遊びました.

21:00からはAtCoder Beginner Contest 277に参加しました. 終了後はAtCoder反省会の中で,AtCoder Beginner Contest 277や追いコンの問題について振り返りました.

追いコンの結果と振り返り

追いコンの結果は以下の通りでした.

参加者の得点と順位

以下に追いコンの振り返りとして,良かった点,悪かった点,今後に向けてするべきことを示します.

良かった点

  • 参加者や協力者を集めることができた.
  • 自分の作成した問題を解いてもらえるのがとても嬉しかった.
  • オンラインジャッジシステムの準備と操作の仕方を習得できた.
  • 全員1問以上ACすることができた.
  • 全問ACしてしまう参加者が出なかった.
  • 得点のグラデーションが緩やかで,問題の構成や配点が適切だった.
  • 協力者と互いにチェックできたため,問題に大きな不備が無かった.
  • 当日,協力者と順位表を見ながらおしゃべりを楽しめた.
  • AmongUsも楽しかった.
  • AtCoder Beginner Contest 277の直前での開催だったため,ウォーミングアップにもなった.

悪かった点

  • オンラインジャッジシステムを動作させているVPSの時刻が狂っていて,終了時刻前に終了してしまった(終了時刻を設定し直すことで復帰するができた.).
  • Discordで質問を受け付けたが,質問内容や質問への回答が参加者全体に共有されてしまうことがあった.

今後に向けてするべきこと

  • VPSの時刻合わせをする.
  • 質問方法について検討する.
  • 今回は,追いコンということでサークルでお世話になった後輩に参加してもらったが,次は,他の知り合いや会社の同僚にも参加してもらえるようなコンテストを企画したい.

まとめ

  • 博士課程を修了したため,それまでお世話になったプログラミングサークルの後輩に感謝の思いを込めて追いコンを開催しました.
  • QingdaoU/OnlineJudgeというオンラインジャッジシステムを利用しました.
  • 参加者全員に楽しんでもらえるように,問題の構成と配点を工夫しました.
  • 問題の作成はとても時間がかかりましたが,みんなに解いてもらえてとても嬉しかったです.

謝辞

追いコンに参加してもらった方々に感謝します.

APG4bとSiv3Dを利用して勉強会を開催した

はじめに

今年度,サークル内で,APG4bとSiv3Dを利用して2つのプログラミング勉強会をオンラインで開催しました. APG4bは,AtCoderC++学習用の資料です.

atcoder.jp

Siv3Dは,可視化やインタラクションに関わるプログラムを作成するためのフレームワークです.

siv3d.github.io

本記事では,開催したプログラミング勉強会について,それぞれその目的や内容などについてまとめます. その上で,APG4bとSiv3Dを利用して良かったと感じたことや勉強会の反省などについてそれぞれまとめます.

本記事はSiv3D Advent Calendar 2021の12月19日の記事です.

qiita.com

APG4bを利用したプログラミング勉強会

APG4bを利用して以下のようにプログラミング勉強会を開催しました.

目的

この勉強会の目的は以下の3つでした.

  • 参加者にプログラミングの楽しさを知ってもらうこと
  • 参加者にプログラミングに興味を持つ人と交流してもらうこと
  • 参加者にプログラミングの基礎知識を身につけてもらうこと

期間

2021年5月12日から2021年7月2日まで間,毎週1回1.5時間程度の勉強会を開催しました.

対象者

この勉強会の対象者は,私の所属サークル内で,初心者を含む全ての部員でした.

講師陣

この勉強会には,私を含めて8名が講師として参加しました. それぞれの講師は,最大で2回の勉強会を担当し,司会と解説を行いました.

内容

内容
1回目 1.00.はじめに,1.01.出力とコメント,1.02.プログラムの書き方とエラー,1.03.四則演算と優先順位
2回目 1.04.変数と型,1.05.プログラムの実行順序と入力
3回目 1.06.if文・比較演算子・論理演算子,1.07.条件式の結果とbool型,1.08.変数のスコープ
4回目 1.09.複合代入演算子,1.10.while文,1.11.for文・break・continue
5回目 1.12.文字列と文字,演習
6回目 1.13.配列,演習
7回目 1.14.STLの関数,1.15.関数
8回目 2.01.ループの書き方と範囲for文,2.02.多重ループ,2.03.多次元配列
9回目 2.05.再帰関数
10回目 3.04.構造体
11回目 3.05.ビット演算

APG4bを利用することにした理由

資料がそれ自体で完結している

自分で資料を探して調べながらプログラミングを勉強することは,プログラミングに初めて触れる初心者の多くにとっては難しいことだと考えています. 実際,私も最初は一通りのC言語を先輩から教えてもらうことでプログラミングを勉強しました.

APG4bは,資料がそれ自体で完結しており,後ろで述べるようなプログラミングの基本知識がこの資料だけで学べるようになっていると思いました. ここには,演習問題も含まれており,このおかげで,講師達がオリジナルの資料や演習問題を用意する手間を省くことができると考えました.

また,資料がそれ自体で完結していることにより,参加者が講師の解説を聞かないで自分のペースで進めることも可能となっていると思いました.

プログラミングの基本知識が網羅されている

私は,型,配列,構造体,変数,四則演算,繰り返し,条件分岐,関数のように多くのプログラミング言語で共通して存在するような概念をプログラミングの基本知識だと考えています. APG4bはこれらの基本知識を網羅しており,プログラミングに初めて触れる人が,自分で調べて勉強できるようになるために必要なキーワードを把握できるものとなっていると思いました.

Web上に実行環境が用意されている

プログラミングに初めて触れる人にとって実行環境を構築することは簡単なことではなく,ここで挫折してしまうのは悲しいと思います. APG4bには,コードを書いてプログラムを実行できるコードテスト機能が備わっており,環境構築に手間をかけなくてもプログラミングの勉強を始められるようになっていると考えました.

ここで勉強したC++をSiv3Dによる開発に応用できる

APG4bで学べるC++は,Siv3Dを使った開発にそのまま生かすことができ,相性が良いと考えました.

その他工夫したこと

この勉強会では,APG4bを利用して準備の手間を省いたことの他にも,交流の時間を設けるという工夫をしました. 交流の時間とは,勉強会の後に,AmongUs,麻雀といったゲームで遊んだり勉強会の復習をしたりする時間です. これを設けることによって部員同士が仲良くなれば,部員の幽霊化を抑えることができると考えました.

振り返り

APG4bの資料をそのまま使ったため,準備に手間をかけずにプログラミング勉強会を開催できたと思います. また,元の資料がそれ自体で完結しているため,講師の力量や相性によらず,最低限の質は保たれていたと思います.

交流の時間を設けたことで,部員の幽霊化を抑えることができたと考えています. 昨年度,似たような形式で勉強会を開催したときは,最初10人程度いた参加者が最後には2人まで減ってしまいました. 交流の時間を設けるというアイデアは,ここでのの反省の結果として生まれたものでした. 実際,今年度の参加者は,最初20人くらいで最後は15人くらいだったと記憶していますが,昨年度ほど大きく参加者が減ることは無かったのは,交流の時間を設けた効果が少なからずあっただろうと考えています.

また,AtCoderのコンテンツを利用した勉強会だったため,そのままAtCoderのコンテストに参加する部員も多く,競技プログラミング勢が増えました.

改善すべき点として,参加者の理解度をもっと把握できるようにすることが挙げられると考えています. AtCoderの提出一覧から提出されたコードを確認できるため,これを利用すれば参加者ごとにアドバイスをしたり,参加者がどこで躓いているか把握することができたと思います. そのために,可能なら事前にユーザ名を教えてもらったり,解けない場合にも提出してもらうように言っておけば良かったと考えています.

Siv3Dを利用したゲーム開発勉強会

OpenSiv3D v0.4.3の資料を利用してゲーム開発勉強会を以下のように開催しました.

目的

この勉強会の目的は,参加者に,ゲーム開発など,AtCoder以外のプログラミングにも興味を持ってもらうことでした.

期間

2021年8月27日から2021年8月30日まで間,毎日午前中に2時間,午後に2時間の勉強会を開催しました.

対象者

この勉強会の対象者は,私の所属サークル内で,ゲーム開発などグラフィカルなアプリの開発に興味がある部員でした.

講師陣

この勉強会には,私を含めて3名が講師として参加しました.

内容

内容
1日目午前 構造体と関数についての復習
1日目午後 Visual Studioの使い方,追加の演習問題
2日目午前 Siv3Dのインストール,1. Siv3D の基本
2日目午後 2. 図形を描く(2.1 円を描く,2.2 色を付ける,2.3 背景の色を変える,2.4 長方形を描く,2.5 枠を描く)
3日目午前 3. 動きを作る(3.1 経過時間を使ったアニメーション(Scene::Time(),Scene::Center(),Scene::DeltaTime()),3.2 ストップウォッチ)
3日目午後 4. あたり判定
4日目午前 5. 画像を描く(5.1 絵文字を描画する,5.5 アイコンを描画する,5.6 画像ファイルを読み込んで描画する)
4日目午後 8. フォントを使う(8.1 Font,8.8 フォントのスタイルを変える,8.13 縦書きでテキストを描画する,8.14 指定した長方形の中にテキストを描く,8.15 テキストを 1 文字ずつ表示する)

Siv3Dを利用することにした理由

日本語の公式資料が充実している

Siv3Dの公式資料では,環境構築からライブラリの使い方までが日本語で示されています. 以前にDXライブラリというものを使用した時には,新しいプロジェクトを作成した後に必要な設定の手順が多くて大変だった思い出があります. これに対して,Siv3Dでは環境構築自体もVisual Studioのインストールと,Siv3Dのインストーラを起動することだけで難易度が低めだと感じています. また,Siv3Dの公式資料は,単なるリファレンスではなく,機能の解説がコピペで動作確認できる短いサンプルコードとともに示されており,動かしたり改造しながら勉強できると思いました.

APG4bで勉強したC++を使用できる

Siv3Dを利用すると,APG4bで勉強したC++によるプログラミングの延長として,アプリケーション開発を行えると考えています. そういう点でSiv3DはAPG4bと相性が良いと思いました. これに対し,例えばUnityでは,エディタとフレームワークを利用する中の一部にプログラミングがあるという感じがしますし,また,使用できるプログラミング言語C++ではなくC#です.

その他工夫したこと

この勉強会では,Siv3Dを利用して準備の手間を省いたことの他にも,以下の工夫をしました.

  • 構造体の復習を設けた.
  • 独自のSiv3D演習問題を設けた.
構造体の復習

Siv3Dはクラスベースのフレームワークであるため,構造体への理解が重要であると考えました. そこで,APG4bで学んだ構造体を復習する時間を最初に設けました. 具体的には,APG4bの3.04構造体のキーポイント,細かい話(コンストラクタ)について解説し,演習として,EX24 - 時計の実装に取り組みました.

また,独自の演習問題として以下を解きました.

  • 構造体に関する追加の演習問題

構造体の練習として、和とスカラー倍の定義されたベクトル表す構造体を実装してみましょう。 以下のプログラムの雛形のコメントに従って構造体を実装してください。 github.com

この演習問題は,Visual Studioの使い方に慣れるためにも,APG4bのコードテストではなく以下を参考にしてVisual Studioで解くこととしました.

www.fenet.jp

独自のSiv3D演習問題

2日目以降は,Siv3Dの演習問題を以下のように準備しました.

演習問題
2日目午前 Siv3Dのサンプルを5個選びそれぞれコピペして実行してみる.
2日目午後 5種類の図形を別の色で描画してみる.
3日目午前 マウスカーソルがウィンドウの左側にある間は円を左に動かし,右側にある間は円を右に等速度で動かしてみる.
3日目午後 クリックした円を分裂させる(元の円を消し,少しずらした位置に二つの円を描画する.).
4日目午前 アイコン,画像を拡大縮小,回転させてみる(5.2 テクスチャを拡大縮小して描画する,5.3 テクスチャを回転して描画するを参考にする.).
4日目午後 3人のキャラクタを表示し,クリックされたキャラクタに何か喋らせる.

難易度としては,サンプルコードを組み合わせたり,少し改造することで解けるものを設定しました. 解答例は以下のリポジトリにあります.

github.com

振り返り

Siv3D(Open Siv3D v0.4.3)の資料をそのまま利用したため,演習問題以外の多くの準備の手間を省くことができたと考えています. また,少なくともゲーム開発勉強会を通して,参加者が今後アプリケーションなどを作りたいと思った時にSiv3Dのことを思い出してもらえるようになったかなと思っています.

しかしながら,改善すべき点として,勉強会中に参加者が演習問題をどの程度解けているのか把握できなかったことが挙げられると考えています. 実際,サンプルコードの実行ができていない参加者が何人かいたようだったのですが,勉強会中には把握できませんでした. 勉強会はオンラインで開催されたため,参加者から声がかからない限り講師が参加者の様子を把握することはできません. これに対する解決案として,勉強会中に参加者が質問をしたりヘルプを求めたりしやすいようにするために,演習ではグループに分かれてそれぞれのグループに講師がつくことなどが挙げられるのかなと考えました.

APG4bを利用したC++勉強会で触れることのできなかったC++文法がSiv3Dのサンプルで使用されていることも多いので,これに対するフォローをもっと手厚くするべきだったと思いました.

また,この勉強会は夏休み中に集中して開催されたのですが,せっかくなら合宿らしく,夜にはオンラインでできるゲームをして盛り上がればもっと楽しくなったかもしれないと思いました.

勉強会を開催する中で,私と他の講師の一人がそれぞれ資料中に誤植を発見し,これを修正するプルリクエストを送ってマージされるということもありました.

MacBook Air (M1, 2020)で環境を構築するためには,以下の記事を参考に設定を修正する必要がありました.

qiita.com

まとめ

  • 今年度,サークル内で,以下のプログラミング勉強会を開催した.
    • APG4bを利用したプログラミング勉強会
    • Siv3Dを利用したゲーム開発勉強会
  • 勉強会の資料としてAPG4bやSiv3Dを利用することには,以下のような多くのメリットがあった.
    • 資料が網羅的であり,それ自体で内容が完結している.
    • 資料をそのまま利用することで講師陣の負担を減らすことができる.
    • 環境構築の難易度が高くない.
    • APG4bやSiv3Dの相性が良い.
  • これらのコンテンツを活用するために以下の工夫をした.
    • APG4bを利用したプログラミング勉強会では,交流の時間を設けた.
    • Siv3Dを利用したゲーム開発勉強会では,構造体の復習の時間と独自の演習問題を設けた.
  • 個人的に反省した結果,これらのコンテンツをオンライン勉強会で最大限活用するための工夫として以下のものが考えられた.
    • AtCoderの提出一覧を意識的に活用できるようにする.
    • 演習では小グループに分かれてそれぞれにコーチがつくようにする.

謝辞

  • 素晴らしい資料とプラットフォームを提供してくれているAtCoderに感謝します.
  • 素晴らしい資料とフレームワークを提供してくれているSiv3Dに感謝します.
  • 講師を含めて,勉強会に参加してくれた方に感謝します.

AtCoder反省会してたはずが気が付くと規格読みながらcpprefjpにプルリク投げてた

はじめに

本記事では,私の所属サークルでAtCoderコンテストABC218の反省会していたはずだったのに,気が付くとC++の規格書を読みながらcpprefjpにプルリクエストを送信していた,という話をします. 本記事は,室蘭工業大学 Advent Calendar 2021の3日目に割り当てられた記事です.

adventar.org

本記事の内容は,【LT会】Acompany競技プログラミングLT会 #1で発表したLTの内容をもとにしています.

acompany.connpass.com

経緯

まず,C++の規格書を読みながらcpprefjpの記事を修正するまでの経緯を述べます.

私の所属サークルのAtCoder反省会

私の所属サークルにはAtCoder反省会という会があります. これは,AtCoderのコンテストの時間になったら,オンラインで集合してコンテストに参加し,コンテスト終了後にそのコンテストの問題の解き直し,感想戦,解説などを行う会です. この会は,競技プログラミングをする部員同士で交流することを目的として始まりました.

ABC218

2021年9月11日に開催されたABC218では,100分間のコンテストの内82分経過した時点でA,B,D,Cを解くことができ,残り18分間でEに挑みました.

atcoder.jp

辺を報酬に関して昇順に並べた上で,グラフが連結になるまで,報酬が低い方からまだ連結していないノード同士を繋ぐ辺をグラフに追加していけば良さそうだなあと感じ,DSUを使って実装しました. しかしながら,結果はWrong Answerで,コンテスト中にデバッグを完了することができませんでした.

atcoder.jp

ABC218のAtCoder反省会

ABC218が終了した後,所属サークルのAtCoder反省会が始まりました. Eの解説をチラッとだけ見てみると,コンテスト中に思いついた解法は想定解法と同じっぽい雰囲気がありました. 運の悪いことにサンプルのテストケースは通ってしまっていたので,テストケースをいくつか自作してプログラムの内部状態の変化を確認しました. そうすると,辺を報酬に関して昇順に並べる処理に失敗していることに気が付きました. この処理は以下のようにしてstd::multiset<tuple<ll,ll,ll>>型の変数Cに辺の情報を追加することで実装していました.

/* 省略 */
using ll = /** __int128; //*/ std::int_fast64_t;
/* 省略 */
void solve(){
  /* 省略 */
  auto cmp = [&](tuple<ll, ll, ll> const &t0, tuple<ll, ll, ll> const &t1) {
    return get<0>(t0) < get<0>(t1) || get<1>(t0) < get<1>(t1) ||
           get<2>(t0) < get<2>(t1);
  };
  std::multiset<tuple<ll, ll, ll>, decltype(cmp)> C(cmp);
  /* 省略 */
}

ここで,Cにはそれぞれの辺の情報が次のようなタプルとして格納されます.

  • 要素として報酬,一つ目のノード,二つ目のノードをこの順に持つタプル

また,Cに格納される辺が報酬に関して昇順に並ぶようにするために,コンパレータcmpを指定しています.

このコンパレータは,とりあえずtuple<ll, ll, ll>のコンパレータのデフォルトの実装と同じものを用意しておいて,後で考察しながらいじれるようにするつもりでした(実際にはデフォルトの実装からいじる必要はありませんでした.). しかし,デフォルトの実装のつもりで記述したコンパレータが実際にはデフォルトの実装と異なったものとなっていました. これが,Wrong Answerを解消できなかった根本的な原因であり,ここに誤りがある限りいくら考察を進めてもAcceptedには辿り着けません.

コンパレータの実装の誤りに気が付いたので,以下のサイトの記事を読んで正しい実装を確認することとしました.

cpprefjp.github.io

この記事を読んでいると,記事のコードの中に「`」が紛れ込んでいるのを見つけたため,この1文字を削除するだけのFix typoプルリクエストを軽い気持ちで送りました.

github.com

その時点では,「翌日にはマージされているだろう,また一つ,Fix typoのプルリクエストを送ってしまった」と思っていました.

プルリクエストがマージされるまで

翌日,プルリクエストにコメントが送られました. 私は,コメントの内容を以下の通りに解釈しました.

  • 私がプルリクエストを送る前の説明が,そもそも不完全であったため,それを完全なものとする必要がある.
  • 私がプルリクエストを送る前の説明が,C++17における説明であり,C++20においての説明にする必要がある.

私は,元のプルリクエストの修正範囲を超えていると感じ,プルリクエストを閉じようとしました. そうしたところ,C++17における説明を完全なものとすることにも意義があるため,その修正をこのプルリクエストでやったらどうかという主旨のコメントが追加されました. C++の規格書として参照すべきURLも与えられていたため,これを見ながら修正を行いました. 修正は,基本的に規格書の内容を日本語訳することにより試みました. これに合わせて,プルリクエストのタイトルを変更し,レビューをお願いしました.

これに対して返されたレビューについて,私は以下の通りに解釈しました.

  • 修正内容には問題は無い.
  • コミットの単位をsquashにより修正するべきである.

squashをするのが初めてだった私が少し自信なさげに「やってみます」と返答したところ,なんとレビューコメントにsquashのやり方まで追記してくれたのでした. コミットのsquashを行うと,プルリクエストは無事マージされました.

私のFix typoでないプルリクエストで,個人的なプロジェクト以外のリポジトリにマージされたのはこれが初めてでした.

Fix typo以外でマージされた初めてのプルリクエス

ABC218のE問題のAccepted

コンパレータはstd::tupleのデフォルトの動作で十分であると考察したため,デフォルトのコンパレータを使用するように変更したところ,デバッグも順調に進み無事Acceptedとなりました.

atcoder.jp

誤りのあったコンパレータを,あえて明示的にデフォルトの動作と同様の動作をするように修正すると以下のようになります.

/* 省略 */
using ll = /** __int128; //*/ std::int_fast64_t;
/* 省略 */
void solve(){
  /* 省略 */
  auto cmp = [&](tuple<ll, ll, ll> const &t0, tuple<ll, ll, ll> const &t1) {
    return get<0>(t0) < get<0>(t1) ||
           ((!(get<0>(t1) < get<0>(t0))) &&
            (get<1>(t0) < get<1>(t1) ||
             ((!(get<1>(t1) < get<1>(t0))) && get<2>(t0) < get<2>(t1))));
  };
  /* 省略 */
  std::multiset<tuple<ll, ll, ll>, decltype(cmp)> C(cmp);
  /* 省略 */
}

【LT会】Acompany競技プログラミングLT会 #1

私の友人の所属先が開催するLT会で,競技プログラミングに関するテーマでLTを募集していました. 私も何か発表してみたいなあと考え,上記の内容を5分程度にまとめてLTを申し込みました. LTに対しては,予想していたよりも良い反応をいただくことができ,発表した甲斐があったと思いました. また,LT後の雑談も盛り上がり,Androidアプリの開発やRustをおすすめされたりしました.

私のLTの他に,2つのLTがありました. 一つは,作問をすることにより,解けなくても思考を続ける粘り強さを養いましょう!という内容でした. 解ける解けないといったことの他に,考察そのものの楽しさを忘れないようにしたいと思いました. もう一つは,ICPCに出場しましょう!という内容でした. ICPCに出場したり,コーチとしてついて行ったりしたことを思い出して,懐かしく感じました.

LT会の後で,LTの内容が競技プログラミングと少しずれていたかもしれないと思いました. 友人にそのことを言ってみると,ずらし方が巧妙で気が付かなかったと言われました.

まとめ

やったこと

  • 所属サークルのAtCoder反省会でABC218Eをデバッグした.
  • cpprefjpのtypoを発見してプルリクエストを投げた.
  • 元の説明が不完全だったため,規格を読んで書き直した.

得たこと

  • ABC218EでAcceptedとなるコードを提出できた.
  • C++の規格を読む」という体験をした.
  • コミットの内容だけでなく,形式まで意識してプルリクエストを送信した.
  • Fix typo以外のプルリクが,自分以外のリポジトリに初めてマージされた!

「KurachanActionGame」の開発に関する個人的な振り返り

私は大学で所属しているプログラミングサークルで以下のようなアクションゲームを共同開発しました.

@sakaki_tohruさんには,プレイ動画を公開してくださいましたことについて,感謝いたします.

このゲームでは,プレイヤが「クラちゃん」(クラゲのような生物のイラスト)をクリックすると,クラちゃんは分裂し,プレイヤは得点を得ます. プレイヤはできるだけ高い合計得点を得ることを目指します.

このゲームに関する詳細な情報は,GitHubリポジトリを参照してください.

github.com

以下では,このゲーム開発プロジェクトに関して個人的に振り返ります.

動機

本ゲーム開発を実行しよう思い至ったのは,以下の気持ちが絡まって溶け合い,弾けた結果でした.

  • サークルの同学年の中で進学したのが自分だけで,さびしかった.
  • 前年度,サークル活動が全くできていなかったため,サークルで何かしたいという気持ちが高まっていた.
  • サークルで,既にAPG4b勉強会とSiv3D勉強会が開催されており,ゲーム開発の下地が整っていると感じていた.
  • サークルで,AtCoder勢力に対抗するためゲーム開発勢力を増やしたかった.

プロジェクトの開始

サークルのリモート部会に頻繁に遊びに来る3名に声をかけました. また,これを聞きつけて自分も参加したいと言ってくれた1名も加えて,全部で5名のチームでゲーム開発プロジェクトをスタートしました. サークル内で共同開発する上で工夫した点の一つは連絡手段でした. サークルのDiscordサーバ内のチャンネルの中に本ゲーム開発用の公開スレッドを作成し,本ゲーム開発での連絡は全てこれを利用しました. これにより,以下の効果が得られると考えました.

  • サークル内ではプロジェクト外のメンバにも情報が公開されるため,進行状況がサークル内で共有される.また,後輩が後から参照して参考にすることもできる.
  • スレッドは時間が経つと自動的にアーカイブされるため,プロジェクト終了後はチャンネル内で邪魔にならない.
  • サークル内ではスレッドがそもそも公開されているため,メッセージの送り先を間違えて別のチャンネルやスレッドに送ってしまっても被害が出にくい.

実際,自分も参加したいと言ってくれた1名は,本プロジェクト用のスレッドのやりとりを見たことで本プロジェクトの存在を知ってくれました.

開発期間

2021年9月12日に最初の打ち合わせを開き,2021年10月31日のリリースを行いました. リリースまでの間,毎週2〜4回集まって,毎回2時間くらい開発を行いました. 全員集まれない回もありましたが,集まれる人だけゆるく集まって開発を行いました.

開発場所

開発期間中は対面で集まって開発することができませんでした. そのため,開発はGather.town ( https://www.gather.town ) というビデオチャットプラットフォーム上で行いました. Gather.townは,Discordなどのグループ通話に比べて,「部室に集まってる感じ」があって良かったと思っています.

バージョン管理

学部の頃からずっとGitとGitHubでバージョン管理しながらプログラムを書いてきた私は,バージョン管理されていないプログラムに触わると心が不安に蝕まれるようになっていました. また,複数人で一つの成果物を作り上げる時のバージョン管理方法として,GitとGitHubを利用したものより良い方法が思いつきませんでした.

GitとGitHubを利用したバージョン管理は,慣れていないメンバにとって非常に高いハードルでしたが,GitとGitHubを使ってもらえるように使い方を教えました.

役割分担

開発では以下のように役割分担をしました.

  • 1年生2人,2年生1人:タイトルシーン,ゲームシーン,クレジットシーン,ルールシーンの開発
  • 4年生1人:上記以外のプログラムの作成,プログラミングのサポート
  • 私:イラストの作成,プログラミングのサポート,日程調整

ここでプログラミングのサポートとは,画面共有してデバッグを手伝ったり,設計やアルゴリズムに関する相談を受けたり,レビューと動作確認したり,C++,Siv3D,Git,GitHubVisual Studioなどについて教えたりといったものです. また,日程調整とは,各回の終了時に「次はいつ集まれますか?」と聞いて次回集まる日時を決めるものです. どんなにプログラミングの得意なメンバを集めたとしても誰かが日程調整をしないとプロジェクトは空中分解してしまいます.実際,過去に一度そうして空中分解したプロジェクトに参加したことがあります.

上記ような役割分担にしたのは,以下が理由となっています.

  • 私以外のメンバが,できるだけプログラミングの楽しさに触れられるようにしたかった.
  • 1,2年生をできるだけ手厚くサポートしたかった.
  • オリジナルのイラストが描いて,それがゲームの中で動くさまを見てみたかった.

また,各シーンの開発では基本的にシーン単位で役割を分担しました. 各メンバには,それぞれの担当するシーンについて,動きやデザインの実装を任せました. これにより,それぞれの作業の独立させやすくなり,作業の競合を避けることができると考えました. 欠点として,全体の統一感が損なわれがちになることが挙げられますが,今回の開発ではそれは気にしないこととしていました.

成果物

まず,ゲーム開発の最低限の要求として,遊ぶことが可能な実行ファイルをダウンロードできる状態にする,ということは達成されました. プログラミング経験の浅い1年生もいるチームでゲームを完成させたという成果は,個人的に満足して良いものだと感じています.

一方で,成果物は以下に挙げるような問題点も抱えています.

  • 「パラちゃん」という既存のゲームに類似しており,新規性があるとは言い難い.
  • ハイスコアデータがテキストファイルで保存されるため,プレイヤによる不正が容易である.
  • 全体のデザインなどに統一感が無い.
  • macOS版ではBGMが正常に再生されない.

しかしながら,面白いゲームを作ることや完成度の高いゲームを作ることは私がこのプロジェクトを開始した動機には含まれていないため,これらの問題点については個人的にはあまり気になっていません.

一応,オープンソースとしてソースコードも公開してますので,もし改善案などがあれば,Issue,Pull request,forkなどは歓迎します.

作成したイラスト

個人的な成果として,作成したイラスト「クラちゃん」を以下に示します.

良かったこと

まず,前述の成果物を完成させて大きな達成感を得られたことはとても良かったです. 以下では,このゲーム開発を通じてと感じたものづくりの楽しさと共同開発の楽しさについて述べます.

ものづくりの楽しさ

開発中,ゲームが少しずつ出来ていく様子や自分が描いたイラストがゲーム内で動くようになっていく様子を見ていると楽しかったです. 私の役割の一つであるイラストでは,「クラちゃん」というクラゲのような生物を自分の手によってこの世に生まれさせることが創造的で楽しかったです.

共同開発の楽しさ

今回の開発では他のメンバのプログラミングのサポートもしましたが,イラストの作成に注力することができたと思っています.実際,感覚的には自分の作業時間の50%くらいはイラストの作成をしていた気がします. これは,すなわち,他のメンバにコーディングを任せていたということであり,他のメンバと協力してゲームを完成させたという実感の源泉にもなっています.

また,今回のプロジェクトにはこれまで自分が参加した共同開発ではあまり感じたことのなかった感動もありました. 今まで共同開発は,同学年でチームを組んでいたのですが,このプロジェクトではチームは下級生とチームを組みました. そして,プロジェクトはチームのメンバにプログラミングを教えながら進めました. このような進め方は初めてでしたが,チームのメンバがプログラミングに慣れていく様子やアドバイスしたことが実装に反映されていく様子を見て感動していました.

上級生にプログラミングのサポートに割り当てるという今回の役割分担は,プログラミングの得意なメンバだけが作業を独占してしまうということが発生せず,メンバ全員が開発に関わることができたため,正解だったと感じています.

悪かったこと

プロジェクトの名前の決め方の雑さ

開始時にプロジェクトの名前を決めていませんでした. これはゲームタイトルが決定してから,それをプロジェクト名にしようと思っていたためです. 「パラちゃん」のようなアクションゲームを作ろうということだけは決まっていたため,GitHubに組織とリポジトリを作成するときに雑に「ParachanActionGame」と命名してしまいました. これは良くありませんでした. なぜなら,リポジトリの名前は後から変更できるのですが,組織の名前は後から変更できないからです.

プロジェクト名はゲームタイトルとは独立に最初に決めるべきでした.

プログラミングのサポートの見通しの甘さ

プログラミングの勉強を始めてわずか半年の1年生が2人いる中で,GitとGitHubでバージョン管理しながら,クラスベースのプログラミングを進めていくというのは,振り返ってみると非常にチャレンジングだったと感じています. それでも,プロジェクト開始前は,私がプログラミングのサポートをしながらプロジェクトを無理なく進めることはできるだろうと思っていましたが,その見通しは甘いものでした. プロジェクトを進める中でプログラミングのサポートを実際に行ってみると,一つのことを教えるためにはその背景にある多くのことを教える必要があり,とてもハードであるということに気が付きました.

今回はラッキーなことに,自分から参加したいと言ってくれた4年生がメンバに加わってくれたため,2人で協力しながらプログラミングのサポートをすることができ,何とかゲームを完成させるまで至ることができたと思っています.

まとめ

  • サークルで幅広い学年のメンバで構成されたチームを結成し,ゲームを共同開発した.
  • KurachanActionGameという名前のアクションゲームを完成させ,実行ファイルをGitHubで一般に公開した.
  • プログラミングやバージョン管理に慣れてないメンバのサポートをしながらプロジェクトを進めた.
  • 開発中はものづくりや共同開発の楽しさを感じ,完成時には達成感を味わった.

謝辞

プロジェクトに付き合ってくれたメンバの皆さんには,一緒に開発してくれたことについて大変感謝しております. 本当にありがとうございました! これからもよろしくお願いいたします!