TL;DR
やりたいこと
関数に対してcase classをそのまま渡して実行したい。
scala > def printProfile(name: String, age: Int): Unit = println(s"${age}歳の${name}です")
scala > case class Profile(name: String, age: Int)
scala > val p = Profile("山田", 25)
scala > printProfile(p.name, p.age) // いちいち引数を書くのが面倒くさい!
25歳の山田です
scala > printProfile.よしなにしてくれるメソッド(p) // case classインスタンスpを渡すだけで実行してほしい!
25歳の山田です
できるようにしました → bigwheel/case-class-expand-and-apply
ライブラリを実装して次のように実行できるようにしました1。
scala > (printProfile _).expandAndApply(p)
25歳の山田です
ライブラリの利用方法はいつもの感じです。READMEを参照してください。
Scala 2.13で追加されるpipeメソッドに合わせたもの(expandAndApplyのレシーバと引数を入れ替えたもの)も使えます。
scala > p.expandAndPipe(printProfile _)
25歳の山田です
背景詳細
scala > def printProfile(name: String, age: Int): Unit = println(s"${age}歳の${name}です")
こういうメソッドがあるとします。
普通に呼び出すときは
scala> printProfile("山田", 25)
25歳の山田です
これで良いですが、この引数を1セットで引き回したいときがあります。
そんなときはTupleを使う手があります。
scala> val profile = ("山田", 25)
profile: (String, Int) = (山田,25)
scala> printProfile(profile._1, profile._2)
25歳の山田です
手で_1, _2と展開するのが面倒になったときは以下の書き方もあります(参考元)2。
scala> (printProfile _).tupled(profile)
25歳の山田です
ただ、Tupleは中身の型とその順番でのみ一意性が定義されます。
言い換えると、中身の値の型とその順番が一緒だとまったく意図していないインスタンスでも以下のように実行できてしまいます。
scala> val item = ("ファンタ", 120)
scala> (printProfile _).tupled(item)
120歳のファンタです
そこでcase classが導入されるのは自然な流れです3。
scala> case class Profile(name: String, age: Int)
scala> val ccProfile = Profile("山田", 25)
ただしcase classはTupleではないのでそのままだとまた手書きで展開する必要があります。
scala> printProfile(ccProfile.name, ccProfile.age)
25歳の山田です
Tupleで一度解決した問題がまた発生しました。
case classはCompanion Objectにunapplyというcase classインスタンスを対応するTupleへ変換するメソッドが必ず定義されます。それを利用すれば以下のようにできます。
scala> (printProfile _).tupled(Profile.unapply(ccProfile).get)
25歳の山田です
ですが Profile.unapply(ccProfile).get
のところにぎこちなさを感じます。
確実に変換できることが保証されているにもかかわらず unapply
の仕様上 Option
型でくるんで返されるためgetメソッドを必ず書いてやる必要があります。またコンパニオンオブジェクトである Profile
のメソッドを呼んでいることも微妙な気がします。ccProfile.toTuple
みたいなインスタンスメソッドがほしいですね。
あります。shapelessライブラリを使いましょう。
scala> import shapeless.syntax.std.product._
scala> (printProfile _).tupled(ccProfile.toTuple)
25歳の山田です
これでかなりマシになりました。
ただ、論理的には「関数へcase classインスタンスを渡す」というように表現したいにもかかわらずコード中でTupleを中継していることが気になります。
implicit classを使いFunction2にメソッドを追加してTupleを隠蔽しましょう。
scala> :paste
// Entering paste mode (ctrl-D to finish)
import shapeless.ops.product.ToTuple
implicit class RichFunction2[T1, T2, R](f: (T1, T2) => R) {
type ARGUMENTS = (T1, T2)
def expandAndApply[CASECLAZZ <: Product](t: CASECLAZZ)
(implicit toTuple: ToTuple.Aux[CASECLAZZ, ARGUMENTS]): R = {
f.tupled(t.toTuple[ARGUMENTS])
}
}
// Exiting paste mode, now interpreting.
scala> (printProfile _).expandAndApply(ccProfile)
25歳の山田です
やりました! scalaの言語仕様ギリギリまでシンプルに表現できたと思います。
ただしこのRichFunctionNは1から22のすべてのFunctionNトレイトに対して定義してやる必要があるのでかなり手間です。
そこでライブラリ化した、というのが今回の背景になります。
残タスク
今回やりたかったことはほぼほぼ達成したのですが、今回のコードは結局関数とcase classを引き合わせるために合流点でTupleを使っている関係上実はcase classの型安全性は損なわれています。
どういうことかというとつまり変数の型とその順番さえ一致していればどのようなcase classでもexpandAndApplyを呼び出せるようになっているということです。Tupleの項目で言及してものと同じ問題が発生しているんですね。
間違ったcase classを渡す危険性を少しでも下げるために、例えば各引数とcase classのメンバーの変数名が一致するかをマクロでチェックしてもいいかもしれません。
偶然変数の型と順番が一致することはあっても、変数名が一致することはそうそうないだろうという前提に基づいた工夫ですね。
ただまああくまで小手先に過ぎない上、変数名を厳密に一致させる必要があるというデメリットも生まれてしまうため微妙な気もします。やるとしても expandAndApply
とは別の名前にしたほうが良さそうですね( expandAndApplyStrictly
など)。
作成動機
そもそもなぜこんなライブラリを作ったかというと、Pythonのようにcase classの各要素を引数リストへunpackしたいと思ったことがきっかけです。
実用性や筋の良し悪しはともかく、作成中はマクロを勉強したりScala本体のFunctionX, Productなどのコードを読んだりで非常に楽しかったので良い経験になりました。
注意書き(あるいは免責)
このやりたいことに対して手段が適当でない気もずっとしていますが、こういった方法もあるよということで作ってみました。
Scalaのそこそこの歴史でもこういったことが具体的にしたい、または実現したというような話がないのでみんな必要性を感じていないか、そもそも解決法が間違っているのではないかと思います。
そもそも筋が悪い、こっちがScala way的なツッコミあればコメントぜひお願いします(丸投げ)。
参考元
- parameters - scala tuple unpacking - Stack Overflow
- Feature overview: shapeless 2.1.0 · milessabin/shapeless Wiki
- Unpack a list in Python? - Stack Overflow
-
expandAndApplyというメソッド名がダサいとかメソッドを記号化したほうが(
<<|
とかね)使いやすくない?とかあるんですがいい名前が浮かばなかったのとscalaオフィシャルでもメソッドの記号表記は今逆風っぽいので記号化は一旦していません。真っ当な理由があったらメソッドのリネーム、エイリアス定義は前向きに検討しますのでプルリクください ↩ -
ちなみに
printProfile
をFunction2型として扱うためのxxx _
はScalaの言語仕様上避けられないようです。正直ここはちょっとイケてないなと思います。トレードオフになりますが、例えば関数実行には必ず()
をつける、という文法にしてしまえばxxx _
は要らなくなるでしょう ↩ -
実際のところ、scalaではcase classが簡単に定義できること、上記のTupleの型的な危険性から動的型付け言語ではTupleが使われるシーンのほとんどでcase classが使われます。Tupleが使われるときはcase classの定義すら省略したいほど局所的に値の組み合わせを一緒に引き回したいときで、具体的には無名関数を定義するとき、複数の値をメソッドの返り値として返したいときなどがあります ↩