「Learning Functional Programming in Go」を読んだ
はじめに
Packt Publishing の Learning Functional Programming in Go を読みました。
この本は、Go で関数型プログラミングをやってみようというなかなかニッチな内容になっています。名前がアレなのでもしかしたら、Functional Programming in Scala みたいな内容を想像された方もいらっしゃると思いますが、確かに FP の重要性は伝えてくれるものの、言語の柔軟性を活かしたエレガントなコードを学ぶための本にはなっていないです。この本はどちらかというと「今の言語仕様つらい。じゃあどうするか」を学ぶための本です。
実はこの本を読み始めた当初は、err != nil
をはじめとしたボイラープレートを排除して簡潔な記述を手に入れたいという願望がありました。それに対する解決策は一応書かれています。でも、どうも雰囲気が違うなというのは、Chapter1 を読んでいるときに気付きました。
この本が伝えようとしていること
では、この本で何が語られているかをざっくりと紹介しますね。
まず最初に、Go で FP をやるためには欠けているものがいくつかあります。一例を挙げると、
などです。
TCO がサポートされていないので、いくら再帰を末尾呼び出しで書いたとしてもコンパイラにガン無視され最適化してくれません。また、Generics が無いので関数を一般化することができずコピペの嵐となります。仮に interface{}
で汎用的に処理しようとすれば今度はリフレクションやキャストなどの選択を迫られることになりパフォーマンス上問題になってしまいます。そう、どうあがいても命令型プログラミングとコピペという結末に収束してしまうのです。
だけど、Go には高階関数があるじゃないか!
ということで、以降は高階関数を使用した Functor、Monoid、Monad へと物語は進んで行きます。時折もし generics があったらなら、あんなことやこんなこともできるのにという著者の熱い想いに触れながら。
結局のところ、今の Go で FP をやるには色々妥協しなくちゃいけなくて、この本では「可能な限り関数型プログラミングスタイルを。必要に応じて命令型プログラミングスタイルを使用する」というスタンスで必死に抗い続けます。つらい。つらすぎる。
だが、そのつらさがオレに執念を与えた。だから、無かったことにするわけには行かなかったのだ。(岡部倫太郎)
その執念が、Go2 への活力となり、公式の Proposing Changes to Go で積極的に議論に参加するよう呼びかけています。
TCO だとこの辺。
generics だとこの辺ですか。
幸いにして、 Go 2 Draft Designs では、 generics の話が出てきているので、Go2 ではもう少し FP がやりやすくなるのかなと期待しています。
おわりに
この本はそれ以外にも SOLID原則やマイクロサービス、DDD、CQRS など、アーキテクチャレベルでのアプローチについても触れられていて、特に Go の Duck Typing 前提のデザインパターンは面白いなと思いました。また、Go2 に generics が入るのはメリットだけじゃなくてコンパイル時間が長くなるという懸念があることも触れています。それが今後どうなるのか注目したいところです。
個人的には、TCO も generics もサポートされた Go2 で、改めて「Learning Functional Programming in Go 2」を出版していただきたいと思ってます。
追記(2018/08/31)
補足すると、本ではSOLID原則のことを紹介してますが、すべてが Go でできるわけじゃないので、例えばリスコフの置換原則について継承のない Go ではどう考えるかみたいな内容になっています。
HHKBの装備(記録用)
私は HHKB Professional2 Type-S を尊師スタイルで使用していますが、今回はその HHKB の装備について書きたいと思います。(至って普通装備ですが...)
キーボードブリッジ
キーボードブリッジは、ノートPC本体のキーボードの上に HHKB を乗せて打鍵するためのキーボードカバーです。尊師スタイルには必須のアイテムです。
HHKB吸振マットHG(Professionalシリーズ用)
吸振マットを HHKB の裏面に装着すると、打鍵の振動・反発を吸収してくれるので、タイピングが安定します。更に表面がエンボス加工になっていてキーボードブリッジに吸い付くように固定されるので、ノーマルのものより安定するんじゃないかなと思ってます(ノーマルは使ったことは無いですが...)あと、若干ですが打鍵音も静かになるようです。
HHKB吸振マットHG(Professionalシリーズ用)|PFUダイレクト
ケーブル
昔の MacBook Pro(Early 2015) であれば、
- USB miniBオス to タイプAオス(リール式)
- 変換名人
を使用しています。ケーブルの方は100円均一で購入しました。
一方、Type C な MacBook Pro 2017 以降の場合は、これを使っています。
ケーブルは、amazonで星一つですが問題なく使えています。
終わりに
Type C to MiniB のケーブルは今のだとちょっとかっこ悪いのでまだまだ研究が必要そうです。それと今後はカラーキートップを付けてみたりと、至って普通のカスタマイズをしていこうかと思ってます。
Scalaの名前渡しパラメータが参照する度に評価されるってどこに明記されてるか調べてみた
恥ずかしい話なんですが、Scala の名前渡しパラメータ (by-name parameter) がメソッドの中でパラメータを参照する度に評価されてしまうことを知りませんでした。つまりこういうことですね。
def f: Int = { println("hello") 0 } def g(v: => Int) = { v + v } g(f) // => 実行結果 // hello // hello
幸いにして、今まで名前渡ししたパラメータをメソッドの中で複数回参照するような実装はしたことがなかったのですが、それにしても今まで何で気付けなかったのかなと思い、公式ドキュメントや書籍でどのように記載されているのかを調べてみました。
Scala Documentation
まずは、Scala Documentation ですが、名前渡しパラメータだとこのあたりですね。
このページでは次のようなサンプルコードが記載されています。
def whileLoop(condition: => Boolean)(body: => Unit): Unit = if (condition) { body whileLoop(condition)(body) } var i = 2 whileLoop (i > 0) { println(i) i -= 1 } // prints 2 1
確かにこれを見ると、名前渡しパラメータが毎回評価されることを前提とした実装になっています。また、
By-name parameters are only evaluated when used.
と書いてありますが、「使うときだけ評価される」というのが毎回という意味も含んでいたのか... まあちゃんと読み解けば気付けましたね...
Functional Programming Principles in Scala
次に Coursera の Functional Programming Principles in Scala。これの Lecture 1.3 - Evaluation Strategies and Termination の中で、Martin Odersky さんは以下のように仰ってます。
In Scala, we normally use call by value. You might ask, well, given the advantages of call by name that it terminates more often, why call by value? Well, it turns out that, for expressions in practice, call by value is often exponentially more efficient than call by name because it avoids this repeated recomputation of argument expressions that call by name entails. The other argument for call by value is that it plays much nicer with, imperative effects and side effects because you tend to know much better when expressions will be evaluated.Since Scala is also an, an imperative side, call by value is the standard choice. Except that, sometimes you really want to force call by name, and Scala lets you do that also. You do that by adding a, an arrow in front of the parameter type.
以下は、Coursera の和訳
Scalaでは通常は値渡しを使います。名前渡しに(引数に渡した無限ループが)停止しやすい利点があるのになぜ値渡しを使うのかと訝しむかもしれません。現実の式では値渡しの方が名前渡しよりも指数的に効率的であることが分かっています。なぜなら値渡しは名前渡しが必要とする引数の式の再計算が不要だからです。値渡しに有利になる別の主張はそれが命令的な作用や副作用をずっと上手く扱うことができるというものです。なぜならいつ式が評価されるのかがずっとわかりやすい傾向があるからです。Scalaもまた命令的な側面がありますので、値渡しが標準的な選択となります
めっちゃ言ってる...
値渡しは名前渡しが必要とする引数の式の再計算が不要だからです。
これも私が聞き逃してただけですねorz
Scalaスケーラブルプログラミング第3版
次にコップ本ですが、名前渡しパラメータについては「9.5 名前渡しパラメータ」に記載があって、if
や while
に近い制御構造を作る際にパラメータの predicate: () => Boolean
から冗長な ()
を省略して predicate: => Boolean
にしたのを名前渡しパラメータとして紹介されています。
確かに元の形が () => Boolean
で Function0
と同じと考えれば毎回評価されるのは納得がいきます。
実際にこちらで紹介されているように名前渡しパラメータは Function0
に置き換わるようですしね。
まあそれは良いとして、本には「毎回評価されるよ」と明記されているわけじゃないので、気付けた人だけが知っているみたいなのはちょっと嫌だなと思ってました。
ところが、コップ本の最後の方を何となく見ていたらちゃんとそこに書いてあることに後から気付きました。コップ本の最後の方の「用語集」にはこんな感じで書かれています。
名前渡しパラメーター(by-name parameter)
たとえば(x: => Int)のように型の前に=>が付けられているパラメータ(仮引数)。名前渡しパラメーターに対応する実引数は、メソッドが呼び出される前ではなく、メソッド内でパラメータが名前で参照されるたびに評価される。名前渡しでないパラメータは、値渡しである。
これも私が見落としてただけでしたか...
おわりに
最近会社の若者たちに Scala を教えてるのですが、今回の件はその中で気付くことができました。なので、若者たちに感謝!
追記(2018/08/22)
「Scala関数型デザイン&プログラミング ―Scalazコントリビューターによる関数型徹底ガイド」を確認したら「5.1 正格関数と非正格関数」で、名前渡ししたパラメータはキャッシュされないので毎回評価される。毎回評価させたくないのであれば明示的にlazyキーワードを使って値をキャッシュさせると書いてありました!もう一度全部読み返さないと!
ScalaでXMLを加工する
Scala で XML を扱う機会ってまだまだあるのかなと思ってます。
例えば、既存システムの WebAPI が XML 形式しか対応してなかった場合とか。そうしたときに、Scala の XML リテラルで直接 XML を書くとか、XML を探索して特定要素の値を抽出するとかだったら情報が比較的手に入れやすいと思います。コップ本にも書いてありますしね。
ところが、既にある XML に対して何らかの加工をしたい場合って情報が少ないんじゃないでしょうか。ということで、今回は Scala で XML を加工する方法について書きたいと思います。(ちなみに私は去年の開発でこの辺りけっこう悩んだ記憶があります。そのときは ここ に実装のヒントが書かれていることを知らなかったので...)
実行環境
実行した環境は以下になります。
準備
build.sbt
の libraryDependencies
に scala-xml
を追加します。
libraryDependencies ++= Seq( "org.scala-lang.modules" %% "scala-xml" % "1.1.0" )
実装概要
今回紹介するのは下記の実装についてです。
- 要素に属性を追加する
- 要素の内容を置換する
- 子要素を置換する
- 子要素を追加する
- 指定した要素を削除する
- 指定した属性の値が条件に合致する場合に要素を削除する
- 子要素が空要素のみの親要素を削除する
- XMLをトリムする
実装方法
要素に属性を追加する
特定の要素に属性を追加する方法です。名前空間なしの場合は UnprefixedAttribute
、名前空間ありの場合は PrefixedAttribute
を使用します。
import scala.xml._ val xml = <root id="hoge"> <a>aaa</a> <b>bbb</b> </root> // 名前空間なし属性の追加 val result1 = xml % new UnprefixedAttribute("key", "value", xml.attributes) println("名前空間なし:") println(result1) println // 名前空間あり属性の追加 val result2 = xml % new PrefixedAttribute("x", "key", "value", xml.attributes) println("名前空間あり:") println(result2)
実行結果
名前空間なし: <root id="hoge" key="value"> <a>aaa</a> <b>bbb</b> </root> 名前空間あり: <root id="hoge" x:key="value"> <a>aaa</a> <b>bbb</b> </root>
要素の内容を置換する
要素の内容の置換には、scala.xml.transform.RuleTransformer
を使用します。
RuleTransformer
のコンストラクタに変換ルールを定義した scala.xml.RewriteRule
を設定するのですが、具体的には transform
メソッドの引数である Node
のパターンマッチで、Elem
クラスで label
が指定した要素の名前だったら、copy
メソッドで置換したい文字列を設定した新しい Elem
を返すようにします。こうすることで、要素の内容が置換されます。
なお、scala.xml.RewriteRule
は複数設定可能なので、変換ルールが分割されている場合にも対応できるようです。
import scala.xml._ import scala.xml.transform._ val xml = <root> <a>aaa</a> <b>bbb</b> </root> val rule = new RuleTransformer(new RewriteRule { override def transform(n: Node): Seq[Node] = n match { case elem: Elem if elem.label == "b" => elem.copy( child = elem.child collect { case Text(_) => Text("ccc") } ) case _ => n } }) val result = rule.transform(xml) println(result)
実行結果
<root> <a>aaa</a> <b>ccc</b> </root>
子要素を置換する
要素の全ての子要素を置換したい場合は、copy
メソッドで指定します。
val xml = <root> <a>aaa</a> <b>bbb</b> </root> val result = xml.copy(child = <c>ccc</c>) println(result)
実行結果
<root><c>ccc</c></root>
子要素を追加する
要素の指定した子要素を追加する場合は、既存の child
に List
と同じ要領で新しい要素を追加します。
val xml = <root> <a>aaa</a> <b>bbb</b> </root> val result = xml.copy(child = <c>ccc</c> ++ xml.child) println(result)
実行結果
<root><c>ccc</c> <a>aaa</a> <b>bbb</b> </root>
指定した要素を削除する
指定した要素を削除する場合はここでも scala.xml.transform.RuleTransformer
を使用します。要素名が合致していたら NodeSeq.Empty
に置き換えています。
import scala.xml._ import scala.xml.transform._ val xml = <root> <a>aaa</a> <b>bbb</b> <b>ccc</b> </root> val rule = new RuleTransformer(new RewriteRule { override def transform(n: Node): NodeSeq = n match { case e: Elem if (e \\ "b") != NodeSeq.Empty => NodeSeq.Empty case _ => n } }) val result = rule.transform(xml) println(result)
実行結果
<root> <a>aaa</a> </root>
NodeSeq.Empty
に置き換えただけなので中身は歯抜けのようになります。この場合は後述しますがトリムをかけると良いです。
指定した属性の値が条件に合致する場合に要素を削除する
以下は、ある要素の配下で属性が flag="true"
の要素を全て削除する例です。
import scala.xml._ import scala.xml.transform._ val xml = <root> <a>aaa</a> <b flag="true">bbb</b> <c flag="false">ccc</c> <d flag="true">ddd</d> </root> val rule = new RuleTransformer(new RewriteRule { override def transform(n: Node): NodeSeq = n match { case e: Elem if (e \ "@flag").text == "true" => NodeSeq.Empty case _ => n } }) val result = rule.transform(xml) println(result)
実行結果
<root> <a>aaa</a> <c flag="false">ccc</c> </root>
子要素が空要素のみの親要素を削除する
以下は、子要素が空要素、または存在しない場合は、親要素もろとも削除する例です。
import scala.xml._ import scala.xml.transform._ val xml = <root> <a> <aa /> </a> <b> <ba>ba</ba> <bb></bb> </b> <c /> </root> val rule = new RuleTransformer(new RewriteRule { override def transform(n: Node): NodeSeq = n match { case elem: Elem if elem.child.count(_.toString.trim != "") == 0 => NodeSeq.Empty case _ => n } }) val result = rule.transform(xml) println(result)
実行結果
<root> <b> <ba>ba</ba> </b> </root>
XMLをトリムする
先ほどまでの実行結果で、XML が歯抜けの場合はトリムをかけることできれいに整形されます。
val xml = <root> <a>aaa</a> <b>bbb</b> </root> val trimXml = scala.xml.Utility.trim(xml) println(trimXml)
実行結果
<root><a>aaa</a><b>bbb</b></root>
参考資料
gibo 2.x系で使い方が微妙に変わっていた
gibo は、github/gitignore で公開されている内容をもとに .gitignoreファイルを自動生成するツールですが、久しぶりに使ってみたら使い方が微妙に変わっていたので自分メモです。
インストール
mac の場合は homebrew でインストールします。
$ brew install gibo
その他の環境は 公式のREADME が参考になります。
バージョン確認
$ gibo version gibo 2.1.0 by Simon Whitaker <sw@netcetera.org> https://github.com/simonwhitaker/gibo
helpを確認
help を確認すると、.gitignore の生成方法が gibo dump
に変わっているのが確認できます。以前は gibo Go >> .gitignore
みたいな使い方でした。
$ gibo help gibo 2.1.0 by Simon Whitaker <sw@netcetera.org> https://github.com/simonwhitaker/gibo Fetches gitignore boilerplates from https://github.com/github/gitignore Usage: gibo [command] Example: gibo dump Swift Xcode >> .gitignore Commands: dump BOILERPLATE... Write boilerplate(s) to STDOUT help Display this help text list List available boilerplates search STR Search for boilerplates with STR in the name update Update list of available boilerplates version Display current script version
自動生成可能な言語や環境を列挙する
以前は gibo --list
でしたが、gibo list
に変わっています。
$ gibo list === Languages === Actionscript Drupal Jekyll Perl Stella Ada Eagle Joomla Perl6 SugarCRM Agda Elisp Julia Phalcon Swift Android Elixir KiCad PlayFramework Symfony AppceleratorTitanium Elm Kohana Plone SymphonyCMS AppEngine EPiServer Kotlin Prestashop Terraform ArchLinuxPackages Erlang LabVIEW Processing TeX Autotools ExpressionEngine Laravel PureScript Textpattern C++ ExtJs Leiningen Python TurboGears2 C Fancy LemonStand Qooxdoo Typo3 CakePHP Finale Lilypond Qt Umbraco CFWheels ForceDotCom Lithium R Unity ChefCookbook Fortran Lua Rails UnrealEngine Clojure FuelPHP Magento RhodesRhomobile VisualStudio CMake Gcov Maven ROS VVVV CodeIgniter GitBook Mercury Ruby Waf CommonLisp Go MetaProgrammingSystem Rust WordPress ~ 以下省略 ~
.gitignoreを自動生成
先ほど help でも確認しましたが、今は gibo dump
を使うみたいですね。さっそく Go の .gitignoreファイルを自動生成してみましょう。
$ gibo dump Go >> .gitignore $ cat .gitignore /vendor/ /.idea gibo 2.1.0 by Simon Whitaker <sw@netcetera.org> https://github.com/simonwhitaker/gibo ### https://raw.github.com/github/gitignore/ea28c14da0faf75047165c10223635ba95566ad7/Go.gitignore # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out
生成できましたね。
今回は以上になります。
circeのdecodeでデフォルト値を設定する
Scala の JSONライブラリ circe で、JSON を decode する際にフィールドが欠落していたらデフォルト値に置き換える方法について書きます。結構前からある機能なのでご存知の方も多いかもしれません。
つまり、こういうケースですね。
import io.circe.generic.auto._ import io.circe.parser._ val json1 = """ |{ | "name": "Taro", | "body": "よろしくお願いします" |} """.stripMargin val json2 = """ |{ | "name": "Taro" |} """.stripMargin final case class Message( name: String, body: String ) // これはうまくいくけど println(decode[Message](json1)) // => Right(Message(Taro,よろしくお願いします)) // フィールドが欠落しているので decode に失敗する。デフォルト値に置き換えたい。 println(decode[Message](json2)) // => Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(body))))
実行環境
今回実行した環境は以下になります。
- Scala 2.12.6
- circe 0.9.3
実現方法
方法1
最初は、case class
のそれぞれのフィールドにデフォルト値を設定しておき、io.circe.generic.extras.Configuration
の指定で欠落するフィールドをデフォルト値へ置き換える方法です。
build.sbt
は以下のように、基本セットの他に circe-generic-extras
を依存に追加します。
libraryDependencies ++= Seq( "io.circe" %% "circe-core", "io.circe" %% "circe-generic", "io.circe" %% "circe-parser", "io.circe" %% "circe-generic-extras" ).map(_ % circeVersion) val circeVersion = "0.9.3"
そして、ポイントとなるのは、
io.circe.generic.auto._
ではなく、io.circe.generic.extras.auto._
を import するimplicit val customConfig: Configuration = Configuration.default.withDefaults
を定義する
コードを見てみましょう。
import io.circe.generic.extras.auto._ import io.circe.generic.extras.Configuration import io.circe.parser._ val json1 = """ |{ | "name": "Taro" |} """.stripMargin val json2 = """ |{ | "body": "よろしくお願いします" |} """.stripMargin val json3 = "{}" // case class のフィールドにはデフォルト値が設定されている final case class Message( name: String = "匿名希望", body: String = "ノーコメントです" ) implicit val customConfig: Configuration = Configuration.default.withDefaults println(decode[Message](json1)) // => Right(Message(Taro,ノーコメントです)) println(decode[Message](json2)) // => Right(Message(匿名希望,よろしくお願いします)) println(decode[Message](json3)) // => Right(Message(匿名希望,ノーコメントです))
このように欠落するフィールドに対してデフォルト値が置き換わりました。
方法2
もう一つの方法は、デフォルト値を設定しておいた case class
を別途用意しておき、それで置き換えるやり方です。
build.sbt
は 方法1 とは異なり、circe-generic-extras
を依存に追加する必要はありません。
libraryDependencies ++= Seq( "io.circe" %% "circe-core", "io.circe" %% "circe-generic", "io.circe" %% "circe-parser" ).map(_ % circeVersion) val circeVersion = "0.9.3"
そして、こうに書きます。
import io.circe.generic.auto._ import io.circe.parser._ val json1 = """ |{ | "name": "Taro" |} """.stripMargin val json2 = """ |{ | "body": "よろしくお願いします" |} """.stripMargin val json3 = "{}" // 方法1と違ってここではデフォルト値は設定していない final case class Message( name: String, body: String ) // 適用させるデフォルト値 lazy val defaultValue = Message("匿名希望", "ノーコメントです") println(decode[Message => Message](json1).map(_(defaultValue))) // => Right(Message(Taro,ノーコメントです)) println(decode[Message => Message](json2).map(_(defaultValue))) // => Right(Message(匿名希望,よろしくお願いします)) println(decode[Message => Message](json3).map(_(defaultValue))) // => Right(Message(匿名希望,ノーコメントです))
この方法でも欠落するフィールドに対してデフォルト値を置き換えることができました。ちなみに以下の issue にもあるように、方法1 の方が公式のやり方っぽいですね。
参考
HHKBでIntelliJ IDEAのショートカットキーのポジションを見直してみる
今年4月に MacBook Pro(以下「MBP」)の標準キーボードから Happy Hacking Keyboard Professional2 Type-S(以下「HHKB」)へ乗り換えました。独特なキー配列に最初は戸惑いを感じましたがそれは数日で慣れ、今では静電容量無接点方式の心地良い打鍵感で快適なタイピングライフを送っています。
ところで、私は普段の開発に IntelliJ IDEA を使用しているのですが、HHKB に変えたことで一部のショートカットキーの指のポジションを見直す必要がありましたので、今回はそんな話をしたいと思います。誰得な話ですが...
なお、対象は以下とします。
それと、今回取り上げるショートカットキーは以下です。
- Rename (Shift + F6)
- Find Usages (Option + F7)
- Terminal (Option + F12)
- File Structure (Command + F12)
Rename (Shift + F6)
変数名やメソッド名などを変更するショートカットキーですが、MBPでホームポジションをなるべく崩さずに打つと次のようになると思います。(写真の青枠は左手、赤枠は右手を意味しています。旧式MBPなのはお気になさらず...)
- 左手小指で左 Shift
- 右手人差し指で F6
ところが、HHKB だと F6 は、Fn と 6 を同時に打たなければならなく、しかもこの二つのキーは物理的に離れているため右手だけでは打つことができません。つまり、こうするのはかなり無理があります。(画像の出典元: Happy Hacking Keyboard キー配列)
そこで、このように変えました。
- 左手人差し指で 6
- 右手薬指で右 Shift
- 右手小指で Fn
Find Usages (Option + F7)
次に、変数やメソッドの参照先を検索する Find Usages ですが、MBPで打つ場合、
- 左手親指で左 Option
- 右手人差し指で F7
これも Rename と同じく右手だけで F7 は無理なので、こうしました。
- 左人差し指で 7
- 右手親指で右 Option
- 右手小指で Fn
Terminal (Option + F12)
IntelliJ IDEA のターミナルを開閉するショートカットキーですが、MBPで打つ場合、
- 左手親指で左 Option
- 右手小指で F12
これに関しては、右手小指で Fn を押すことを考えると、これが自然な気がしてます。
- 右手小指で Fn
- 右手中指で F12
- 右手親指で Option
File Structure (Command + F12)
最後にファイル構造をポップアップで表示してくれる File Structure ですが、MBPで打つ場合、
- 左手親指で左 Command
- 右手小指で F12
これは、Terminal と同じ要領で、こうにしました。
- 右手小指で Fn
- 右手中指で F12
- 右手親指で Command
おわりに
最後まで読んでいただいてありがとうございます。いかがだったでしょうか。
ホームポジション崩れまくりで敗北感すら感じてしまうわけですが、今のところこれが一番しっくりくるので、しばらくはこれでやっていこうと思います!